style: align multiline ternary operators
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Failing after 2m11s
Backend security / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Failing after 2m17s
Conventional commits / commits (pull_request) Successful in 34s
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Failing after 2m11s
Backend security / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Failing after 2m17s
Conventional commits / commits (pull_request) Successful in 34s
This commit is contained in:
@@ -70,9 +70,9 @@ function rangeFor(
|
||||
if (!history) {
|
||||
throw new ServiceUnavailableException({
|
||||
code:
|
||||
input.instrumentType === 'share' ?
|
||||
'STOCK_HISTORY_START_UNAVAILABLE' :
|
||||
'BOND_HISTORY_START_UNAVAILABLE',
|
||||
input.instrumentType === 'share'
|
||||
? 'STOCK_HISTORY_START_UNAVAILABLE'
|
||||
: 'BOND_HISTORY_START_UNAVAILABLE',
|
||||
});
|
||||
}
|
||||
from.setTime(utcDay(new Date(history)).valueOf());
|
||||
@@ -114,9 +114,9 @@ export class CandleService {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
const now = new Date();
|
||||
const range = detail.instrument ?
|
||||
rangeFor(input, detail.instrument, now) :
|
||||
null;
|
||||
const range = detail.instrument
|
||||
? rangeFor(input, detail.instrument, now)
|
||||
: null;
|
||||
const saved = range ? await this.repository.read(input.uid, range) : null;
|
||||
const gaps = range && saved ? candleGaps(range, saved.coverage, now) : [];
|
||||
if (!observe && (!saved || gaps.length)) {
|
||||
@@ -139,14 +139,14 @@ export class CandleService {
|
||||
});
|
||||
return {
|
||||
data:
|
||||
!saved || !covered ?
|
||||
null :
|
||||
{
|
||||
!saved || !covered
|
||||
? null
|
||||
: {
|
||||
candles: saved.data,
|
||||
currency:
|
||||
input.instrumentType === 'share' ?
|
||||
detail.instrument?.currency?.toUpperCase() || null :
|
||||
null,
|
||||
input.instrumentType === 'share'
|
||||
? detail.instrument?.currency?.toUpperCase() || null
|
||||
: null,
|
||||
historyFrom: timestamp(detail?.instrument?.first_1dayCandleDate),
|
||||
observedAt: fetchedAt?.toISOString() ?? now.toISOString(),
|
||||
},
|
||||
@@ -159,8 +159,8 @@ export class CandleService {
|
||||
work,
|
||||
coverage: {
|
||||
requested: range ? [publicRange(range)] : [],
|
||||
completeRanges: range ?
|
||||
completeCoverage.flatMap((row) => {
|
||||
completeRanges: range
|
||||
? completeCoverage.flatMap((row) => {
|
||||
const from = new Date(
|
||||
Math.max(row.from.valueOf(), range.from.valueOf()),
|
||||
);
|
||||
@@ -168,22 +168,22 @@ export class CandleService {
|
||||
Math.min(row.to.valueOf(), range.to.valueOf()),
|
||||
);
|
||||
return from < to ? [publicRange({ from, to })] : [];
|
||||
}) :
|
||||
[],
|
||||
})
|
||||
: [],
|
||||
missingRanges: missing.map(publicRange),
|
||||
asOf: fetchedAt?.toISOString() ?? null,
|
||||
historyBoundary: timestamp(detail?.instrument?.first_1dayCandleDate) ?
|
||||
'known' :
|
||||
'unknown',
|
||||
historyState: complete ?
|
||||
'complete' :
|
||||
work?.state === 'failed' ?
|
||||
'failed' :
|
||||
work?.state === 'running' ?
|
||||
'loading' :
|
||||
covered ?
|
||||
'partial' :
|
||||
'missing',
|
||||
historyBoundary: timestamp(detail?.instrument?.first_1dayCandleDate)
|
||||
? 'known'
|
||||
: 'unknown',
|
||||
historyState: complete
|
||||
? 'complete'
|
||||
: work?.state === 'failed'
|
||||
? 'failed'
|
||||
: work?.state === 'running'
|
||||
? 'loading'
|
||||
: covered
|
||||
? 'partial'
|
||||
: 'missing',
|
||||
},
|
||||
dependencies: [
|
||||
detail.meta,
|
||||
@@ -193,9 +193,9 @@ export class CandleService {
|
||||
dataRevision: String(row.dataRevision),
|
||||
availability: 'complete',
|
||||
fetchedAt: row.fetchedAt,
|
||||
freshForMs: row.isComplete ?
|
||||
Number.POSITIVE_INFINITY :
|
||||
tailFreshForMs,
|
||||
freshForMs: row.isComplete
|
||||
? Number.POSITIVE_INFINITY
|
||||
: tailFreshForMs,
|
||||
work: null,
|
||||
}),
|
||||
),
|
||||
@@ -291,9 +291,9 @@ export class CandleService {
|
||||
) {
|
||||
const input = { uid, instrumentType, period: 'all' as const };
|
||||
const range =
|
||||
detail?.instrument && history ?
|
||||
rangeFor(input, detail.instrument, now) :
|
||||
null;
|
||||
detail?.instrument && history
|
||||
? rangeFor(input, detail.instrument, now)
|
||||
: null;
|
||||
if (
|
||||
!range ||
|
||||
candleGaps(
|
||||
|
||||
@@ -79,14 +79,14 @@ export class CatalogDiscoveryService {
|
||||
|
||||
async search(userId: string, input: CatalogQuery, observe = false) {
|
||||
const discovery = await this.read(userId, input, observe);
|
||||
const sources = discovery.data ?
|
||||
await this.enrichment.readMany(
|
||||
const sources = discovery.data
|
||||
? await this.enrichment.readMany(
|
||||
userId,
|
||||
input.instrumentType,
|
||||
discovery.data.map((item) => item.uid),
|
||||
observe,
|
||||
) :
|
||||
[];
|
||||
)
|
||||
: [];
|
||||
const dependencies = [
|
||||
discovery.meta,
|
||||
...sources.map((source) => source.meta),
|
||||
@@ -114,22 +114,22 @@ export class CatalogDiscoveryService {
|
||||
.map((dependency) => dependency.dataRevision)
|
||||
.join('/'),
|
||||
availability:
|
||||
discovery.data === null ?
|
||||
'missing' :
|
||||
sources.every((source) => source.meta.availability === 'complete') ?
|
||||
'complete' :
|
||||
'partial',
|
||||
fetchedAt: discovery.meta.lastSuccessfulAt ?
|
||||
new Date(discovery.meta.lastSuccessfulAt) :
|
||||
null,
|
||||
discovery.data === null
|
||||
? 'missing'
|
||||
: sources.every((source) => source.meta.availability === 'complete')
|
||||
? 'complete'
|
||||
: 'partial',
|
||||
fetchedAt: discovery.meta.lastSuccessfulAt
|
||||
? new Date(discovery.meta.lastSuccessfulAt)
|
||||
: null,
|
||||
freshForMs: tbankSourceTtlMs.catalog,
|
||||
work: discovery.meta.sync.id ?
|
||||
{
|
||||
work: discovery.meta.sync.id
|
||||
? {
|
||||
id: discovery.meta.sync.id,
|
||||
state: discovery.meta.sync.state,
|
||||
errorCode: discovery.meta.sync.errorCode,
|
||||
} :
|
||||
null,
|
||||
}
|
||||
: null,
|
||||
dependencies,
|
||||
}),
|
||||
};
|
||||
@@ -177,17 +177,17 @@ export class CatalogDiscoveryService {
|
||||
.map((dependency) => dependency.dataRevision)
|
||||
.join('/'),
|
||||
availability: source.meta.availability,
|
||||
fetchedAt: source.meta.lastSuccessfulAt ?
|
||||
new Date(source.meta.lastSuccessfulAt) :
|
||||
null,
|
||||
fetchedAt: source.meta.lastSuccessfulAt
|
||||
? new Date(source.meta.lastSuccessfulAt)
|
||||
: null,
|
||||
freshForMs: tbankSourceTtlMs.metadata,
|
||||
work: source.meta.sync.id ?
|
||||
{
|
||||
work: source.meta.sync.id
|
||||
? {
|
||||
id: source.meta.sync.id,
|
||||
state: source.meta.sync.state,
|
||||
errorCode: source.meta.sync.errorCode,
|
||||
} :
|
||||
null,
|
||||
}
|
||||
: null,
|
||||
dependencies,
|
||||
}),
|
||||
};
|
||||
@@ -208,36 +208,36 @@ export class CatalogDiscoveryService {
|
||||
if (lookup.data && lookup.data.length > UNAMBIGUOUS_MATCH_COUNT) {
|
||||
throw new ConflictException({
|
||||
code:
|
||||
instrumentType === 'share' ?
|
||||
'STOCK_TICKER_AMBIGUOUS' :
|
||||
'BOND_TICKER_AMBIGUOUS',
|
||||
instrumentType === 'share'
|
||||
? 'STOCK_TICKER_AMBIGUOUS'
|
||||
: 'BOND_TICKER_AMBIGUOUS',
|
||||
});
|
||||
}
|
||||
const result = lookup.data ?
|
||||
await this.detail(userId, instrumentType, lookup.data[0].uid, observe) :
|
||||
null;
|
||||
const result = lookup.data
|
||||
? await this.detail(userId, instrumentType, lookup.data[0].uid, observe)
|
||||
: null;
|
||||
const dependencies = [lookup.meta, ...(result ? [result.meta] : [])];
|
||||
return {
|
||||
data: result?.data ?
|
||||
{ ...result.data, slug: ticker.trim().toUpperCase() } :
|
||||
null,
|
||||
data: result?.data
|
||||
? { ...result.data, slug: ticker.trim().toUpperCase() }
|
||||
: null,
|
||||
meta: investmentResourceMeta({
|
||||
resourceKey: `${instrumentType}/ticker/${ticker.trim().toUpperCase()}`,
|
||||
dataRevision: dependencies
|
||||
.map((dependency) => dependency.dataRevision)
|
||||
.join('/'),
|
||||
availability: result?.meta.availability ?? 'missing',
|
||||
fetchedAt: lookup.meta.lastSuccessfulAt ?
|
||||
new Date(lookup.meta.lastSuccessfulAt) :
|
||||
null,
|
||||
fetchedAt: lookup.meta.lastSuccessfulAt
|
||||
? new Date(lookup.meta.lastSuccessfulAt)
|
||||
: null,
|
||||
freshForMs: tbankSourceTtlMs.metadata,
|
||||
work: lookup.meta.sync.id ?
|
||||
{
|
||||
work: lookup.meta.sync.id
|
||||
? {
|
||||
id: lookup.meta.sync.id,
|
||||
state: lookup.meta.sync.state,
|
||||
errorCode: lookup.meta.sync.errorCode,
|
||||
} :
|
||||
null,
|
||||
}
|
||||
: null,
|
||||
dependencies,
|
||||
}),
|
||||
};
|
||||
@@ -334,19 +334,19 @@ export class CatalogDiscoveryService {
|
||||
const token = await this.access.tokenForAttempt(attempt);
|
||||
if (token) {
|
||||
const found =
|
||||
input.instrumentType === 'share' ?
|
||||
await this.tbank.findShares(token, input.query) :
|
||||
await this.tbank.findBonds(token, input.query);
|
||||
input.instrumentType === 'share'
|
||||
? await this.tbank.findShares(token, input.query)
|
||||
: await this.tbank.findBonds(token, input.query);
|
||||
success = await this.repository.publish(
|
||||
attempt,
|
||||
input,
|
||||
selectCatalogInstruments(
|
||||
input.exactTicker ?
|
||||
found.filter(
|
||||
input.exactTicker
|
||||
? found.filter(
|
||||
(item) =>
|
||||
item.ticker?.trim().toUpperCase() === input.query,
|
||||
) :
|
||||
found,
|
||||
)
|
||||
: found,
|
||||
input.query,
|
||||
input.instrumentType,
|
||||
).slice(
|
||||
|
||||
@@ -124,9 +124,9 @@ export class CatalogEnrichmentService {
|
||||
range,
|
||||
);
|
||||
}
|
||||
const work = inputs.length ?
|
||||
await this.repository.workMany(userId, revision, inputs) :
|
||||
[];
|
||||
const work = inputs.length
|
||||
? await this.repository.workMany(userId, revision, inputs)
|
||||
: [];
|
||||
const byScope = new Map(work.map((row) => [row.scopeKey, row]));
|
||||
return inputs.map((input) => {
|
||||
const row = rows.find(
|
||||
@@ -136,16 +136,16 @@ export class CatalogEnrichmentService {
|
||||
const payload = row?.payload as CatalogPayload | undefined;
|
||||
return {
|
||||
input,
|
||||
version: row ?
|
||||
{
|
||||
version: row
|
||||
? {
|
||||
instrumentUid: row.instrumentUid,
|
||||
source: row.source,
|
||||
kind: row.kind,
|
||||
rangeKey: row.rangeKey,
|
||||
dataRevision: row.dataRevision,
|
||||
fetchedAt: row.fetchedAt,
|
||||
} :
|
||||
null,
|
||||
}
|
||||
: null,
|
||||
payload,
|
||||
instrument: payload?.instrument ?? null,
|
||||
price: payload?.price ?? null,
|
||||
@@ -177,9 +177,9 @@ export class CatalogEnrichmentService {
|
||||
) {
|
||||
const inputs = sourceInputs(instrumentType, uids, kinds, range);
|
||||
const reservation = this.reservations.then(async () => {
|
||||
const rows = inputs.length ?
|
||||
await this.repository.readSources(inputs) :
|
||||
[];
|
||||
const rows = inputs.length
|
||||
? await this.repository.readSources(inputs)
|
||||
: [];
|
||||
const ids: string[] = [];
|
||||
for (const input of inputs) {
|
||||
const row = rows.find(
|
||||
@@ -242,18 +242,18 @@ export class CatalogEnrichmentService {
|
||||
observe = false,
|
||||
) {
|
||||
const kinds: CatalogSource['kind'][] =
|
||||
section === 'company' ?
|
||||
['detail', 'description', 'fundamentals'] :
|
||||
[
|
||||
...(instrumentType === 'bond' ?
|
||||
['bond-current' as const] :
|
||||
['detail' as const]),
|
||||
section === 'company'
|
||||
? ['detail', 'description', 'fundamentals']
|
||||
: [
|
||||
...(instrumentType === 'bond'
|
||||
? ['bond-current' as const]
|
||||
: ['detail' as const]),
|
||||
'last-price',
|
||||
'trading-status',
|
||||
'comparison',
|
||||
...(section === 'metrics' ?
|
||||
['coupons' as const, 'yield' as const, 'rating' as const] :
|
||||
[]),
|
||||
...(section === 'metrics'
|
||||
? ['coupons' as const, 'yield' as const, 'rating' as const]
|
||||
: []),
|
||||
];
|
||||
const sources = await this.readMany(
|
||||
userId,
|
||||
@@ -279,18 +279,18 @@ export class CatalogEnrichmentService {
|
||||
dataRevision: dependencies
|
||||
.map((dependency) => dependency.dataRevision)
|
||||
.join('/'),
|
||||
availability: !first.instrument ?
|
||||
'missing' :
|
||||
complete ?
|
||||
'complete' :
|
||||
'partial',
|
||||
fetchedAt: first.meta.lastSuccessfulAt ?
|
||||
new Date(first.meta.lastSuccessfulAt) :
|
||||
null,
|
||||
availability: !first.instrument
|
||||
? 'missing'
|
||||
: complete
|
||||
? 'complete'
|
||||
: 'partial',
|
||||
fetchedAt: first.meta.lastSuccessfulAt
|
||||
? new Date(first.meta.lastSuccessfulAt)
|
||||
: null,
|
||||
freshForMs:
|
||||
section === 'company' ?
|
||||
tbankSourceTtlMs.company :
|
||||
tbankSourceTtlMs.quote,
|
||||
section === 'company'
|
||||
? tbankSourceTtlMs.company
|
||||
: tbankSourceTtlMs.quote,
|
||||
work: null,
|
||||
dependencies,
|
||||
});
|
||||
@@ -315,9 +315,9 @@ export class CatalogEnrichmentService {
|
||||
): Promise<CatalogPayload> {
|
||||
if (input.kind === 'detail' || input.kind === 'bond-current') {
|
||||
const instrument =
|
||||
input.instrumentType === 'share' ?
|
||||
await this.tbank.getShare(token, input.uid) :
|
||||
await this.tbank.getBond(token, input.uid);
|
||||
input.instrumentType === 'share'
|
||||
? await this.tbank.getShare(token, input.uid)
|
||||
: await this.tbank.getBond(token, input.uid);
|
||||
if (instrument && instrument.uid !== input.uid) {
|
||||
throw new Error('Catalog UID mismatch');
|
||||
}
|
||||
@@ -400,9 +400,9 @@ export class CatalogEnrichmentService {
|
||||
}
|
||||
const details = await this.repository.readSources([
|
||||
{ ...input, kind: 'detail', rangeKey: undefined },
|
||||
...(input.instrumentType === 'bond' ?
|
||||
[{ ...input, kind: 'bond-current' as const, rangeKey: undefined }] :
|
||||
[]),
|
||||
...(input.instrumentType === 'bond'
|
||||
? [{ ...input, kind: 'bond-current' as const, rangeKey: undefined }]
|
||||
: []),
|
||||
]);
|
||||
const instrument = details
|
||||
.map((row) => (row.payload as CatalogPayload).instrument)
|
||||
@@ -415,36 +415,36 @@ export class CatalogEnrichmentService {
|
||||
}
|
||||
if (input.kind === 'description') {
|
||||
return {
|
||||
description: instrument?.assetUid ?
|
||||
await this.tbank.getAssetDescription(token, instrument?.assetUid) :
|
||||
null,
|
||||
description: instrument?.assetUid
|
||||
? await this.tbank.getAssetDescription(token, instrument?.assetUid)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
if (input.kind === 'fundamentals') {
|
||||
return {
|
||||
fundamentals: instrument?.assetUid ?
|
||||
await this.tbank.getFundamentals(token, instrument?.assetUid) :
|
||||
null,
|
||||
fundamentals: instrument?.assetUid
|
||||
? await this.tbank.getFundamentals(token, instrument?.assetUid)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
if (input.kind === 'coupons' || input.kind === 'dividends') {
|
||||
const year = Number(input.rangeKey);
|
||||
const [from, to] = input.rangeKey?.startsWith('[') ?
|
||||
(JSON.parse(input.rangeKey) as [string, string]) :
|
||||
[
|
||||
const [from, to] = input.rangeKey?.startsWith('[')
|
||||
? (JSON.parse(input.rangeKey) as [string, string])
|
||||
: [
|
||||
`${year - payoutHistoryYears}-01-01`,
|
||||
`${year + payoutFutureYears}-01-01`,
|
||||
];
|
||||
return input.kind === 'coupons' ?
|
||||
{
|
||||
return input.kind === 'coupons'
|
||||
? {
|
||||
coupons: await this.tbank.getBondCoupons(
|
||||
token,
|
||||
input.uid,
|
||||
from,
|
||||
to,
|
||||
),
|
||||
} :
|
||||
{
|
||||
}
|
||||
: {
|
||||
dividends: await this.tbank.getDividends(
|
||||
token,
|
||||
input.uid,
|
||||
@@ -461,20 +461,20 @@ export class CatalogEnrichmentService {
|
||||
if (input.kind === 'yield') {
|
||||
return {
|
||||
effectiveYield:
|
||||
instrument?.ticker && instrument.isin && instrument.classCode ?
|
||||
await this.moex.bondYield(
|
||||
instrument?.ticker && instrument.isin && instrument.classCode
|
||||
? await this.moex.bondYield(
|
||||
instrument.ticker,
|
||||
instrument.isin,
|
||||
instrument.classCode,
|
||||
) :
|
||||
null,
|
||||
)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
if (input.kind === 'rating') {
|
||||
return {
|
||||
creditRating: instrument?.isin ?
|
||||
await this.ratings.rating(instrument.isin) :
|
||||
null,
|
||||
creditRating: instrument?.isin
|
||||
? await this.ratings.rating(instrument.isin)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
throw new Error('Unsupported catalog source');
|
||||
@@ -533,17 +533,17 @@ export class CatalogEnrichmentService {
|
||||
prices?.find((entry) => entry.instrumentUid === item.input.uid) ??
|
||||
null;
|
||||
const value =
|
||||
item.input.kind === 'last-price' ?
|
||||
{
|
||||
price: price ?
|
||||
{
|
||||
item.input.kind === 'last-price'
|
||||
? {
|
||||
price: price
|
||||
? {
|
||||
instrumentUid: item.input.uid,
|
||||
price: price.price,
|
||||
time: price.time,
|
||||
} :
|
||||
null,
|
||||
} :
|
||||
payload;
|
||||
}
|
||||
: null,
|
||||
}
|
||||
: payload;
|
||||
// Keep accepting observers while publication awaits SQLite.
|
||||
item.observers.unshift({ attempt: item.attempt, input: item.input });
|
||||
for (const observer of item.observers) {
|
||||
@@ -596,17 +596,17 @@ function sourceInputs(
|
||||
uid,
|
||||
kind,
|
||||
// eslint-disable-next-line security/detect-object-injection -- Numeric queue indexes and validated instrument UIDs select internal data only.
|
||||
...(kind === 'bond-events' && (range?.tickers?.[uid] || range?.ticker) ?
|
||||
...(kind === 'bond-events' && (range?.tickers?.[uid] || range?.ticker)
|
||||
// eslint-disable-next-line security/detect-object-injection -- Numeric queue indexes and validated instrument UIDs select internal data only.
|
||||
{ ticker: range?.tickers?.[uid] || range?.ticker } :
|
||||
{}),
|
||||
...(['coupons', 'dividends'].includes(kind) ?
|
||||
{
|
||||
rangeKey: range ?
|
||||
JSON.stringify([range.from, range.to]) :
|
||||
String(new Date().getUTCFullYear()),
|
||||
} :
|
||||
{}),
|
||||
? { ticker: range?.tickers?.[uid] || range?.ticker }
|
||||
: {}),
|
||||
...(['coupons', 'dividends'].includes(kind)
|
||||
? {
|
||||
rangeKey: range
|
||||
? JSON.stringify([range.from, range.to])
|
||||
: String(new Date().getUTCFullYear()),
|
||||
}
|
||||
: {}),
|
||||
})),
|
||||
);
|
||||
}
|
||||
@@ -679,9 +679,9 @@ function sourcePolicy(kind: CatalogSource['kind']) {
|
||||
}
|
||||
function producerKey(input: CatalogSource) {
|
||||
return input.instrumentType === 'bond' &&
|
||||
(input.kind === 'detail' || input.kind === 'bond-current') ?
|
||||
`bond-metadata:${input.uid}` :
|
||||
catalogScope(input);
|
||||
(input.kind === 'detail' || input.kind === 'bond-current')
|
||||
? `bond-metadata:${input.uid}`
|
||||
: catalogScope(input);
|
||||
}
|
||||
|
||||
const quotationScale = 9;
|
||||
|
||||
@@ -9,9 +9,9 @@ export function selectCatalogInstruments<T extends TbankShare>(
|
||||
): (T & { uid: string })[] {
|
||||
const seen = new Set<string>();
|
||||
const kind =
|
||||
instrumentType === 'share' ?
|
||||
'INSTRUMENT_TYPE_SHARE' :
|
||||
'INSTRUMENT_TYPE_BOND';
|
||||
instrumentType === 'share'
|
||||
? 'INSTRUMENT_TYPE_SHARE'
|
||||
: 'INSTRUMENT_TYPE_BOND';
|
||||
const preferredClass = instrumentType === 'share' ? 'TQBR' : 'TQOB';
|
||||
return found
|
||||
.filter(
|
||||
@@ -29,9 +29,9 @@ export function selectCatalogInstruments<T extends TbankShare>(
|
||||
a.uid.localeCompare(b.uid),
|
||||
)
|
||||
.filter((instrument) => {
|
||||
const key = instrument.isin?.trim() ?
|
||||
`isin:${instrument.isin.trim().toUpperCase()}` :
|
||||
`uid:${instrument.uid}`;
|
||||
const key = instrument.isin?.trim()
|
||||
? `isin:${instrument.isin.trim().toUpperCase()}`
|
||||
: `uid:${instrument.uid}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
|
||||
@@ -10,9 +10,9 @@ const MIN_TIMESTAMP_SECONDS_EXCLUSIVE = 0;
|
||||
const MOSCOW_OFFSET_MS = 10_800_000;
|
||||
export function timestamp(value?: TbankTimestamp): null | string {
|
||||
const seconds = Number(value?.seconds);
|
||||
return Number.isFinite(seconds) && seconds > MIN_TIMESTAMP_SECONDS_EXCLUSIVE ?
|
||||
new Date(seconds * MILLISECONDS_PER_SECOND).toISOString() :
|
||||
null;
|
||||
return Number.isFinite(seconds) && seconds > MIN_TIMESTAMP_SECONDS_EXCLUSIVE
|
||||
? new Date(seconds * MILLISECONDS_PER_SECOND).toISOString()
|
||||
: null;
|
||||
}
|
||||
export function moscowDay(iso: string): string {
|
||||
return new Date(Date.parse(iso) + MOSCOW_OFFSET_MS)
|
||||
|
||||
@@ -43,8 +43,8 @@ export function instrumentPosition(
|
||||
currentValue,
|
||||
absoluteReturn,
|
||||
averagePurchasePrice:
|
||||
positive && totalCost ?
|
||||
{
|
||||
positive && totalCost
|
||||
? {
|
||||
currency: totalCost.currency,
|
||||
value: formatDecimal(
|
||||
divide(
|
||||
@@ -53,15 +53,15 @@ export function instrumentPosition(
|
||||
PRICE_DECIMAL_PLACES,
|
||||
),
|
||||
),
|
||||
} :
|
||||
null,
|
||||
}
|
||||
: null,
|
||||
returnPercent: positive ? percent(absoluteReturn, totalCost) : null,
|
||||
accounts: positions.map((p) => ({
|
||||
...p,
|
||||
returnPercent:
|
||||
decimalFromString(p.quantity).value > ZERO_QUANTITY ?
|
||||
percent(p.absoluteReturn, cost(p)) :
|
||||
null,
|
||||
decimalFromString(p.quantity).value > ZERO_QUANTITY
|
||||
? percent(p.absoluteReturn, cost(p))
|
||||
: null,
|
||||
})),
|
||||
isStale: portfolio.isStale,
|
||||
observedAt: portfolio.lastSuccessfulAt,
|
||||
@@ -69,8 +69,8 @@ export function instrumentPosition(
|
||||
}
|
||||
function cost(position: PortfolioPositionDto): MoneyDto | null {
|
||||
const price = position.averagePurchasePrice;
|
||||
return price ?
|
||||
{
|
||||
return price
|
||||
? {
|
||||
currency: price.currency,
|
||||
value: formatDecimal(
|
||||
multiply(
|
||||
@@ -78,8 +78,8 @@ function cost(position: PortfolioPositionDto): MoneyDto | null {
|
||||
decimalFromString(position.quantity),
|
||||
),
|
||||
),
|
||||
} :
|
||||
null;
|
||||
}
|
||||
: null;
|
||||
}
|
||||
function sumComplete(values: (MoneyDto | null)[]): MoneyDto | null {
|
||||
const currency = values[0]?.currency;
|
||||
|
||||
@@ -37,18 +37,18 @@ export class BondCatalogService {
|
||||
data:
|
||||
result.data?.map((item) => {
|
||||
const short = toBond(item);
|
||||
const detail = item.detail ?
|
||||
toBond({ ...item.detail, uid: item.uid }) :
|
||||
short;
|
||||
const detail = item.detail
|
||||
? toBond({ ...item.detail, uid: item.uid })
|
||||
: short;
|
||||
const last = item.price;
|
||||
return {
|
||||
...detail,
|
||||
classCode: detail.classCode ?? short.classCode,
|
||||
isin: detail.isin ?? short.isin,
|
||||
apiTradeAvailable: short.apiTradeAvailable,
|
||||
pricePercent: last?.price ?
|
||||
formatDecimal(decimal(last.price)) :
|
||||
null,
|
||||
pricePercent: last?.price
|
||||
? formatDecimal(decimal(last.price))
|
||||
: null,
|
||||
priceAt: timestamp(last?.time),
|
||||
};
|
||||
}) ?? null,
|
||||
@@ -63,9 +63,9 @@ export class BondCatalogService {
|
||||
const result = await this.discovery.detail(userId, 'bond', uid, observe);
|
||||
return {
|
||||
...result,
|
||||
data: result.data ?
|
||||
{ ...toBond(result.data), slug: result.data.slug } :
|
||||
null,
|
||||
data: result.data
|
||||
? { ...toBond(result.data), slug: result.data.slug }
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,9 +82,9 @@ export class BondCatalogService {
|
||||
);
|
||||
return {
|
||||
...result,
|
||||
data: result.data ?
|
||||
{ ...toBond(result.data), slug: result.data.slug } :
|
||||
null,
|
||||
data: result.data
|
||||
? { ...toBond(result.data), slug: result.data.slug }
|
||||
: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +62,9 @@ export class BondIncomeService {
|
||||
observe,
|
||||
);
|
||||
return {
|
||||
data: result.values.instrument ?
|
||||
bondMetrics(result.values, result.unavailable, result.observedAt) :
|
||||
null,
|
||||
data: result.values.instrument
|
||||
? bondMetrics(result.values, result.unavailable, result.observedAt)
|
||||
: null,
|
||||
meta: result.meta,
|
||||
};
|
||||
}
|
||||
@@ -99,18 +99,18 @@ export class BondIncomeService {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
const events = sources[1].payload?.coupons;
|
||||
const position = portfolio.data ?
|
||||
instrumentPosition(portfolio.data, uid, 'BOND') :
|
||||
null;
|
||||
const projected = events ?
|
||||
bondCoupons(
|
||||
const position = portfolio.data
|
||||
? instrumentPosition(portfolio.data, uid, 'BOND')
|
||||
: null;
|
||||
const projected = events
|
||||
? bondCoupons(
|
||||
events,
|
||||
position,
|
||||
{ from, to, today },
|
||||
sources[1].meta.lastSuccessfulAt ?? new Date().toISOString(),
|
||||
!portfolio.data,
|
||||
) :
|
||||
undefined;
|
||||
)
|
||||
: undefined;
|
||||
const result = await this.forecasts.result({
|
||||
userId,
|
||||
revision: connection.revision,
|
||||
@@ -121,9 +121,9 @@ export class BondIncomeService {
|
||||
candidate:
|
||||
portfolio.data && sources[0].instrument ? projected : undefined,
|
||||
});
|
||||
return result.data || !projected ?
|
||||
result :
|
||||
{
|
||||
return result.data || !projected
|
||||
? result
|
||||
: {
|
||||
data: bondCoupons(
|
||||
events ?? [],
|
||||
null,
|
||||
@@ -146,19 +146,19 @@ export function bondCoupons(
|
||||
const { from, to, today } = window;
|
||||
const quantity =
|
||||
position?.accounts.length &&
|
||||
position.accounts.every((p) => decimalFromString(p.quantity).value > ZERO_QUANTITY) ?
|
||||
decimalFromString(position.quantity) :
|
||||
null;
|
||||
position.accounts.every((p) => decimalFromString(p.quantity).value > ZERO_QUANTITY)
|
||||
? decimalFromString(position.quantity)
|
||||
: null;
|
||||
const coupons = events
|
||||
.map((event) => {
|
||||
const paymentDate = timestamp(event.couponDate)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null;
|
||||
const recordDate = timestamp(event.fixDate)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null;
|
||||
const perBond = money(event.payOneBond);
|
||||
const timing = !paymentDate ?
|
||||
('undated' as const) :
|
||||
paymentDate >= today ?
|
||||
('upcoming' as const) :
|
||||
('past' as const);
|
||||
const timing = !paymentDate
|
||||
? ('undated' as const)
|
||||
: paymentDate >= today
|
||||
? ('upcoming' as const)
|
||||
: ('past' as const);
|
||||
return {
|
||||
number: event.couponNumber ?? null,
|
||||
paymentDate,
|
||||
@@ -172,14 +172,14 @@ export function bondCoupons(
|
||||
perBond &&
|
||||
timing === 'upcoming' &&
|
||||
recordDate &&
|
||||
recordDate >= today ?
|
||||
{
|
||||
recordDate >= today
|
||||
? {
|
||||
currency: perBond.currency,
|
||||
value: formatDecimal(
|
||||
multiply(quantity, decimalFromString(perBond.value)),
|
||||
),
|
||||
} :
|
||||
null,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (a.paymentDate ?? '').localeCompare(b.paymentDate ?? ''));
|
||||
@@ -225,8 +225,8 @@ export function bondMetrics(
|
||||
clean &&
|
||||
coupon.currency === clean.currency &&
|
||||
decimalFromString(coupon.value).value >= ZERO_AMOUNT &&
|
||||
decimalFromString(clean.value).value > ZERO_AMOUNT ?
|
||||
formatDecimal(
|
||||
decimalFromString(clean.value).value > ZERO_AMOUNT
|
||||
? formatDecimal(
|
||||
divide(
|
||||
multiply(
|
||||
decimalFromString(coupon.value),
|
||||
@@ -238,14 +238,14 @@ export function bondMetrics(
|
||||
),
|
||||
COUPON_YIELD_DECIMAL_PLACES,
|
||||
),
|
||||
) :
|
||||
null;
|
||||
)
|
||||
: null;
|
||||
const couponType =
|
||||
next?.couponType && next.couponType !== 'COUPON_TYPE_UNSPECIFIED' ?
|
||||
next.couponType :
|
||||
instrument.floatingCouponFlag === true ?
|
||||
'COUPON_TYPE_FLOATING' :
|
||||
null;
|
||||
next?.couponType && next.couponType !== 'COUPON_TYPE_UNSPECIFIED'
|
||||
? next.couponType
|
||||
: instrument.floatingCouponFlag === true
|
||||
? 'COUPON_TYPE_FLOATING'
|
||||
: null;
|
||||
return {
|
||||
creditRating,
|
||||
coupon,
|
||||
|
||||
@@ -48,9 +48,9 @@ export class BondMarketService {
|
||||
observe,
|
||||
);
|
||||
return {
|
||||
data: result.values.instrument ?
|
||||
bondQuote(result.values, result.unavailable, result.observedAt) :
|
||||
null,
|
||||
data: result.values.instrument
|
||||
? bondQuote(result.values, result.unavailable, result.observedAt)
|
||||
: null,
|
||||
meta: result.meta,
|
||||
};
|
||||
}
|
||||
@@ -83,8 +83,8 @@ export function bondQuote(
|
||||
const cleanPrice =
|
||||
pricePercent !== null &&
|
||||
nominal &&
|
||||
decimalFromString(nominal.value).value > ZERO_AMOUNT ?
|
||||
{
|
||||
decimalFromString(nominal.value).value > ZERO_AMOUNT
|
||||
? {
|
||||
currency: nominal.currency,
|
||||
value: formatDecimal(
|
||||
divide(
|
||||
@@ -96,11 +96,11 @@ export function bondQuote(
|
||||
PRICE_DECIMAL_PLACES,
|
||||
),
|
||||
),
|
||||
} :
|
||||
null;
|
||||
}
|
||||
: null;
|
||||
const dirtyPrice =
|
||||
cleanPrice && aci && aci.currency === cleanPrice.currency ?
|
||||
{
|
||||
cleanPrice && aci && aci.currency === cleanPrice.currency
|
||||
? {
|
||||
currency: cleanPrice.currency,
|
||||
value: formatDecimal(
|
||||
add(
|
||||
@@ -108,8 +108,8 @@ export function bondQuote(
|
||||
decimalFromString(aci.value),
|
||||
),
|
||||
),
|
||||
} :
|
||||
null;
|
||||
}
|
||||
: null;
|
||||
return {
|
||||
pricePercent,
|
||||
cleanPrice,
|
||||
@@ -118,9 +118,9 @@ export function bondQuote(
|
||||
priceAt,
|
||||
tradingStatus,
|
||||
changePercentagePoints:
|
||||
last?.price && previous?.close ?
|
||||
formatDecimal(subtract(decimal(last.price), decimal(previous.close))) :
|
||||
null,
|
||||
last?.price && previous?.close
|
||||
? formatDecimal(subtract(decimal(last.price), decimal(previous.close)))
|
||||
: null,
|
||||
previousCloseDate: timestamp(previous?.time)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null,
|
||||
unavailable,
|
||||
observedAt,
|
||||
|
||||
@@ -123,9 +123,9 @@ describe('bond financial projections', () => {
|
||||
{
|
||||
couponDate: time('2026-10-10'),
|
||||
payOneBond:
|
||||
scenario === 'unknown' ?
|
||||
undefined :
|
||||
money('30', scenario === 'currency' ? 'usd' : 'rub'),
|
||||
scenario === 'unknown'
|
||||
? undefined
|
||||
: money('30', scenario === 'currency' ? 'usd' : 'rub'),
|
||||
couponPeriod: scenario === 'period' ? 0 : 90,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -4,7 +4,7 @@ import { type TbankMoney } from '../../../../../integrations/tbank/tbank.api';
|
||||
|
||||
export function money(value?: TbankMoney) {
|
||||
const currency = value?.currency?.toUpperCase();
|
||||
return value && currency ?
|
||||
{ currency, value: formatDecimal(decimal(value)) } :
|
||||
null;
|
||||
return value && currency
|
||||
? { currency, value: formatDecimal(decimal(value)) }
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -38,9 +38,9 @@ export function candleGaps(
|
||||
gaps = gaps.flatMap((gap) => {
|
||||
if (covered.to <= gap.from || covered.from >= gap.to) return [gap];
|
||||
return [
|
||||
...(covered.from > gap.from ?
|
||||
[{ from: gap.from, to: covered.from }] :
|
||||
[]),
|
||||
...(covered.from > gap.from
|
||||
? [{ from: gap.from, to: covered.from }]
|
||||
: []),
|
||||
...(covered.to < gap.to ? [{ from: covered.to, to: gap.to }] : []),
|
||||
];
|
||||
});
|
||||
|
||||
@@ -100,14 +100,14 @@ export class CandleRepository {
|
||||
priority: nextPriority,
|
||||
intent: {
|
||||
...previousIntent,
|
||||
...(intent.force ?
|
||||
{
|
||||
...(intent.force
|
||||
? {
|
||||
force: true,
|
||||
...(intent.now && intent.now > previousNow ?
|
||||
{ now: intent.now } :
|
||||
{}),
|
||||
} :
|
||||
{}),
|
||||
...(intent.now && intent.now > previousNow
|
||||
? { now: intent.now }
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -263,12 +263,12 @@ export class CandleRepository {
|
||||
// Preserve coverage outside the requested reconciliation boundaries.
|
||||
for (const previous of old) {
|
||||
for (const part of [
|
||||
...(previous.from < range.from ?
|
||||
[{ from: previous.from, to: range.from }] :
|
||||
[]),
|
||||
...(previous.to > range.to ?
|
||||
[{ from: range.to, to: previous.to }] :
|
||||
[]),
|
||||
...(previous.from < range.from
|
||||
? [{ from: previous.from, to: range.from }]
|
||||
: []),
|
||||
...(previous.to > range.to
|
||||
? [{ from: range.to, to: previous.to }]
|
||||
: []),
|
||||
]) {
|
||||
await tx.tbankCandleCoverage.create({
|
||||
data: { ...previous, ...part },
|
||||
|
||||
+33
-33
@@ -66,8 +66,8 @@ export class CatalogDiscoveryRepository {
|
||||
|
||||
read(input: CatalogQuery) {
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const alias = input.exactTicker ?
|
||||
await tx.tbankInstrumentLookup.findUnique({
|
||||
const alias = input.exactTicker
|
||||
? await tx.tbankInstrumentLookup.findUnique({
|
||||
where: {
|
||||
provider_identifierType_identifier: {
|
||||
provider,
|
||||
@@ -75,17 +75,17 @@ export class CatalogDiscoveryRepository {
|
||||
identifier: input.query,
|
||||
},
|
||||
},
|
||||
}) :
|
||||
null;
|
||||
const discovery = input.exactTicker ?
|
||||
alias ?
|
||||
{
|
||||
})
|
||||
: null;
|
||||
const discovery = input.exactTicker
|
||||
? alias
|
||||
? {
|
||||
instrumentUids: alias.matchedUids,
|
||||
fetchedAt: alias.fetchedAt,
|
||||
dataRevision: alias.dataRevision,
|
||||
} :
|
||||
null :
|
||||
await tx.tbankCatalogDiscovery.findUnique({
|
||||
}
|
||||
: null
|
||||
: await tx.tbankCatalogDiscovery.findUnique({
|
||||
where: {
|
||||
provider_instrumentType_query: {
|
||||
provider,
|
||||
@@ -134,9 +134,9 @@ export class CatalogDiscoveryRepository {
|
||||
kind: input.kind,
|
||||
source: sourceProvider(input),
|
||||
rangeKey:
|
||||
input.kind === 'detail' ?
|
||||
input.instrumentType :
|
||||
(input.rangeKey ?? ''),
|
||||
input.kind === 'detail'
|
||||
? input.instrumentType
|
||||
: (input.rangeKey ?? ''),
|
||||
})),
|
||||
},
|
||||
});
|
||||
@@ -232,9 +232,9 @@ export class CatalogDiscoveryRepository {
|
||||
increment: isDeepStrictEqual(
|
||||
previousAlias?.matchedUids,
|
||||
matchedUids,
|
||||
) ?
|
||||
UNCHANGED_REVISION_INCREMENT :
|
||||
REVISION_INCREMENT,
|
||||
)
|
||||
? UNCHANGED_REVISION_INCREMENT
|
||||
: REVISION_INCREMENT,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -246,9 +246,9 @@ export class CatalogDiscoveryRepository {
|
||||
source: sourceProvider(input),
|
||||
kind: input.kind,
|
||||
rangeKey:
|
||||
input.kind === 'detail' ?
|
||||
input.instrumentType :
|
||||
(input.rangeKey ?? ''),
|
||||
input.kind === 'detail'
|
||||
? input.instrumentType
|
||||
: (input.rangeKey ?? ''),
|
||||
};
|
||||
const previous = await tx.tbankInstrumentSource.findUnique({
|
||||
where: { provider_instrumentUid_source_kind_rangeKey: key },
|
||||
@@ -359,9 +359,9 @@ export class CatalogDiscoveryRepository {
|
||||
instrumentKind: instrument.instrumentKind,
|
||||
apiTradeAvailableFlag: instrument.apiTradeAvailableFlag,
|
||||
currency: instrument.currency,
|
||||
brand: instrument.brand ?
|
||||
{ logoName: instrument.brand.logoName } :
|
||||
undefined,
|
||||
brand: instrument.brand
|
||||
? { logoName: instrument.brand.logoName }
|
||||
: undefined,
|
||||
}),
|
||||
) as Prisma.InputJsonObject;
|
||||
await tx.tbankInstrument.upsert({
|
||||
@@ -418,9 +418,9 @@ export class CatalogDiscoveryRepository {
|
||||
increment: isDeepStrictEqual(
|
||||
previous?.matchedUids,
|
||||
instrumentUids,
|
||||
) ?
|
||||
UNCHANGED_REVISION_INCREMENT :
|
||||
REVISION_INCREMENT,
|
||||
)
|
||||
? UNCHANGED_REVISION_INCREMENT
|
||||
: REVISION_INCREMENT,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -444,9 +444,9 @@ export class CatalogDiscoveryRepository {
|
||||
increment: isDeepStrictEqual(
|
||||
previous?.instrumentUids,
|
||||
instrumentUids,
|
||||
) ?
|
||||
UNCHANGED_REVISION_INCREMENT :
|
||||
REVISION_INCREMENT,
|
||||
)
|
||||
? UNCHANGED_REVISION_INCREMENT
|
||||
: REVISION_INCREMENT,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -480,9 +480,9 @@ export class CatalogDiscoveryRepository {
|
||||
}
|
||||
|
||||
function sourceProvider(input: CatalogSource) {
|
||||
return input.kind === 'yield' || input.kind === 'bond-events' ?
|
||||
'moex' :
|
||||
input.kind === 'rating' ?
|
||||
'ncr' :
|
||||
'tbank';
|
||||
return input.kind === 'yield' || input.kind === 'bond-events'
|
||||
? 'moex'
|
||||
: input.kind === 'rating'
|
||||
? 'ncr'
|
||||
: 'tbank';
|
||||
}
|
||||
|
||||
+16
-16
@@ -23,8 +23,8 @@ export class GetStockCompanyUseCase {
|
||||
observe,
|
||||
);
|
||||
return {
|
||||
data: result.values.instrument ?
|
||||
{
|
||||
data: result.values.instrument
|
||||
? {
|
||||
description: result.values.description ?? null,
|
||||
metrics: metrics(
|
||||
result.values.fundamentals ?? null,
|
||||
@@ -32,8 +32,8 @@ export class GetStockCompanyUseCase {
|
||||
),
|
||||
unavailable: result.unavailable,
|
||||
observedAt: result.observedAt,
|
||||
} :
|
||||
null,
|
||||
}
|
||||
: null,
|
||||
meta: result.meta,
|
||||
};
|
||||
}
|
||||
@@ -51,22 +51,22 @@ export function metrics(
|
||||
][] = [
|
||||
['marketCapitalization', 'money', null],
|
||||
['peRatioTtm', 'ratio', 'TTM'],
|
||||
...(bank ?
|
||||
([
|
||||
...(bank
|
||||
? ([
|
||||
['priceToBookTtm', 'ratio', 'TTM'],
|
||||
['roe', 'percent', null],
|
||||
] as typeof selection) :
|
||||
([
|
||||
] as typeof selection)
|
||||
: ([
|
||||
['revenueTtm', 'money', 'TTM'],
|
||||
['ebitdaTtm', 'money', 'TTM'],
|
||||
] as typeof selection)),
|
||||
['netIncomeTtm', 'money', 'TTM'],
|
||||
...(!bank ?
|
||||
([
|
||||
...(!bank
|
||||
? ([
|
||||
['totalDebtMrq', 'money', 'MRQ'],
|
||||
['netDebtToEbitda', 'ratio', null],
|
||||
] as typeof selection) :
|
||||
[]),
|
||||
] as typeof selection)
|
||||
: []),
|
||||
];
|
||||
return selection.flatMap(([key, unit, period]) => {
|
||||
// eslint-disable-next-line security/detect-object-injection -- Keys come from the fixed metric selection declared above.
|
||||
@@ -74,8 +74,8 @@ export function metrics(
|
||||
const currency = values.currency?.toUpperCase() || null;
|
||||
return typeof value === 'number' &&
|
||||
Number.isFinite(value) &&
|
||||
(unit !== 'money' || currency) ?
|
||||
[
|
||||
(unit !== 'money' || currency)
|
||||
? [
|
||||
{
|
||||
key,
|
||||
value,
|
||||
@@ -83,7 +83,7 @@ export function metrics(
|
||||
period,
|
||||
currency: unit === 'money' ? currency : null,
|
||||
},
|
||||
] :
|
||||
[];
|
||||
]
|
||||
: [];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@ export class StockCatalogService {
|
||||
data:
|
||||
result.data?.map((item) => {
|
||||
const short = stock(item);
|
||||
const detail = item.detail ?
|
||||
stock({ ...item.detail, uid: item.uid }) :
|
||||
short;
|
||||
const detail = item.detail
|
||||
? stock({ ...item.detail, uid: item.uid })
|
||||
: short;
|
||||
const last = item.price;
|
||||
return {
|
||||
...detail,
|
||||
@@ -44,12 +44,12 @@ export class StockCatalogService {
|
||||
isin: detail.isin ?? short.isin,
|
||||
apiTradeAvailable: short.apiTradeAvailable,
|
||||
price:
|
||||
last?.price && detail.currency ?
|
||||
{
|
||||
last?.price && detail.currency
|
||||
? {
|
||||
currency: detail.currency,
|
||||
value: formatDecimal(decimal(last.price)),
|
||||
} :
|
||||
null,
|
||||
}
|
||||
: null,
|
||||
priceAt: timestamp(last?.time),
|
||||
};
|
||||
}) ?? null,
|
||||
@@ -64,9 +64,9 @@ export class StockCatalogService {
|
||||
const result = await this.discovery.detail(userId, 'share', uid, observe);
|
||||
return {
|
||||
...result,
|
||||
data: result.data ?
|
||||
{ ...stock(result.data), slug: result.data.slug } :
|
||||
null,
|
||||
data: result.data
|
||||
? { ...stock(result.data), slug: result.data.slug }
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,9 +83,9 @@ export class StockCatalogService {
|
||||
);
|
||||
return {
|
||||
...result,
|
||||
data: result.data ?
|
||||
{ ...stock(result.data), slug: result.data.slug } :
|
||||
null,
|
||||
data: result.data
|
||||
? { ...stock(result.data), slug: result.data.slug }
|
||||
: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,18 +69,18 @@ export class StockIncomeService {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
const events = sources[1].payload?.dividends;
|
||||
const position = portfolio.data ?
|
||||
instrumentPosition(portfolio.data, uid, 'STOCK') :
|
||||
null;
|
||||
const projected = events ?
|
||||
stockDividends(
|
||||
const position = portfolio.data
|
||||
? instrumentPosition(portfolio.data, uid, 'STOCK')
|
||||
: null;
|
||||
const projected = events
|
||||
? stockDividends(
|
||||
events,
|
||||
position,
|
||||
{ from, to, today },
|
||||
sources[1].meta.lastSuccessfulAt ?? new Date().toISOString(),
|
||||
!portfolio.data,
|
||||
) :
|
||||
undefined;
|
||||
)
|
||||
: undefined;
|
||||
const result = await this.forecasts.result({
|
||||
userId,
|
||||
revision: connection.revision,
|
||||
@@ -91,9 +91,9 @@ export class StockIncomeService {
|
||||
candidate:
|
||||
portfolio.data && sources[0].instrument ? projected : undefined,
|
||||
});
|
||||
return result.data || !projected ?
|
||||
result :
|
||||
{
|
||||
return result.data || !projected
|
||||
? result
|
||||
: {
|
||||
data: stockDividends(
|
||||
events ?? [],
|
||||
null,
|
||||
@@ -117,9 +117,9 @@ export function stockDividends(
|
||||
const quantity =
|
||||
position &&
|
||||
position.accounts.length &&
|
||||
position.accounts.every((p) => decimalFromString(p.quantity).value > ZERO_QUANTITY) ?
|
||||
decimalFromString(position.quantity) :
|
||||
null;
|
||||
position.accounts.every((p) => decimalFromString(p.quantity).value > ZERO_QUANTITY)
|
||||
? decimalFromString(position.quantity)
|
||||
: null;
|
||||
const dividends: StockDividendDto[] = events
|
||||
.filter((event) => event.dividendType?.toLowerCase() !== 'cancelled')
|
||||
.map((event) => {
|
||||
@@ -128,29 +128,29 @@ export function stockDividends(
|
||||
const lastBuyDate = timestamp(event.lastBuyDate)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null;
|
||||
const currency = event.dividendNet?.currency?.toUpperCase();
|
||||
const perShare =
|
||||
currency && event.dividendNet ?
|
||||
{ currency, value: formatDecimal(decimal(event.dividendNet)) } :
|
||||
null;
|
||||
currency && event.dividendNet
|
||||
? { currency, value: formatDecimal(decimal(event.dividendNet)) }
|
||||
: null;
|
||||
// A future payment can have an already-fixed register: keep the event, but do not estimate entitlement from today's holding.
|
||||
const eventDate = paymentDate ?? recordDate;
|
||||
const timing: StockDividendDto['timing'] = eventDate ?
|
||||
eventDate >= today ?
|
||||
'upcoming' :
|
||||
'past' :
|
||||
'undated';
|
||||
const timing: StockDividendDto['timing'] = eventDate
|
||||
? eventDate >= today
|
||||
? 'upcoming'
|
||||
: 'past'
|
||||
: 'undated';
|
||||
const estimatedAmount =
|
||||
quantity &&
|
||||
perShare &&
|
||||
recordDate &&
|
||||
recordDate >= today &&
|
||||
timing === 'upcoming' ?
|
||||
{
|
||||
timing === 'upcoming'
|
||||
? {
|
||||
currency: perShare.currency,
|
||||
value: formatDecimal(
|
||||
multiply(quantity, decimalFromString(perShare.value)),
|
||||
),
|
||||
} :
|
||||
null;
|
||||
}
|
||||
: null;
|
||||
return {
|
||||
perShare,
|
||||
recordDate,
|
||||
|
||||
@@ -44,9 +44,9 @@ export class StockMarketService {
|
||||
observe,
|
||||
);
|
||||
return {
|
||||
data: result.values.instrument ?
|
||||
stockQuote(result.values, result.unavailable, result.observedAt) :
|
||||
null,
|
||||
data: result.values.instrument
|
||||
? stockQuote(result.values, result.unavailable, result.observedAt)
|
||||
: null,
|
||||
meta: result.meta,
|
||||
};
|
||||
}
|
||||
@@ -75,20 +75,20 @@ export function stockQuote(
|
||||
const priceAt = timestamp(last?.time);
|
||||
const currency = share?.currency?.toUpperCase();
|
||||
const price =
|
||||
last?.price && currency ?
|
||||
{ currency, value: formatDecimal(decimal(last.price)) } :
|
||||
null;
|
||||
last?.price && currency
|
||||
? { currency, value: formatDecimal(decimal(last.price)) }
|
||||
: null;
|
||||
const previousPrice =
|
||||
previous?.close && currency ?
|
||||
{ currency, value: formatDecimal(decimal(previous.close)) } :
|
||||
null;
|
||||
previous?.close && currency
|
||||
? { currency, value: formatDecimal(decimal(previous.close)) }
|
||||
: null;
|
||||
const difference =
|
||||
price && previousPrice ?
|
||||
subtract(
|
||||
price && previousPrice
|
||||
? subtract(
|
||||
decimalFromString(price.value),
|
||||
decimalFromString(previousPrice.value),
|
||||
) :
|
||||
null;
|
||||
)
|
||||
: null;
|
||||
return {
|
||||
price,
|
||||
priceAt,
|
||||
@@ -96,21 +96,21 @@ export function stockQuote(
|
||||
previousCloseDate: timestamp(previous?.time)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null,
|
||||
previousClose: previousPrice,
|
||||
change:
|
||||
difference && currency ?
|
||||
{ currency, value: formatDecimal(difference) } :
|
||||
null,
|
||||
difference && currency
|
||||
? { currency, value: formatDecimal(difference) }
|
||||
: null,
|
||||
changePercent:
|
||||
difference &&
|
||||
previousPrice &&
|
||||
decimalFromString(previousPrice.value).value > ZERO_AMOUNT ?
|
||||
formatDecimal(
|
||||
decimalFromString(previousPrice.value).value > ZERO_AMOUNT
|
||||
? formatDecimal(
|
||||
divide(
|
||||
multiply(difference, decimalFromString('100')),
|
||||
decimalFromString(previousPrice.value),
|
||||
CHANGE_PERCENT_DECIMAL_PLACES,
|
||||
),
|
||||
) :
|
||||
null,
|
||||
)
|
||||
: null,
|
||||
unavailable,
|
||||
observedAt,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user