118 lines
3.0 KiB
TypeScript
118 lines
3.0 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
|
|
import {
|
|
decimalFromString,
|
|
divide,
|
|
formatDecimal,
|
|
multiply,
|
|
subtract,
|
|
} from '../../../../../common/decimal';
|
|
import { ISO_DATE_LENGTH, ISO_DATE_START } from '../../../../../common/time';
|
|
import { decimal } from '../../../../../integrations/tbank/decimal';
|
|
import { CandleService } from '../../application/candle.service';
|
|
import {
|
|
CatalogEnrichmentService,
|
|
type CatalogPayload,
|
|
} from '../../application/catalog-enrichment.service';
|
|
import { timestamp } from '../../application/instrument-market';
|
|
import {
|
|
StockCandlesQueryDto,
|
|
StockQuoteDto,
|
|
StockQuoteResourceDto,
|
|
} from '../presentation/stock.dto';
|
|
|
|
const ZERO_AMOUNT = 0n;
|
|
|
|
const CHANGE_PERCENT_DECIMAL_PLACES = 6;
|
|
@Injectable()
|
|
export class StockMarketService {
|
|
constructor(
|
|
private readonly history: CandleService,
|
|
private readonly sources: CatalogEnrichmentService,
|
|
) {}
|
|
|
|
async quote(
|
|
userId: string,
|
|
uid: string,
|
|
observe = false,
|
|
): Promise<StockQuoteResourceDto> {
|
|
const result = await this.sources.section(
|
|
userId,
|
|
'share',
|
|
uid,
|
|
'quote',
|
|
observe,
|
|
);
|
|
return {
|
|
data: result.values.instrument
|
|
? stockQuote(result.values, result.unavailable, result.observedAt)
|
|
: null,
|
|
meta: result.meta,
|
|
};
|
|
}
|
|
|
|
candles(
|
|
userId: string,
|
|
uid: string,
|
|
period: StockCandlesQueryDto['period'] = 'month',
|
|
observe = false,
|
|
) {
|
|
return this.history.read(
|
|
userId,
|
|
{ uid, instrumentType: 'share', period },
|
|
observe,
|
|
);
|
|
}
|
|
}
|
|
|
|
export function stockQuote(
|
|
values: CatalogPayload,
|
|
unavailable: string[],
|
|
observedAt: string,
|
|
): StockQuoteDto {
|
|
const { price: last, candle: previous, tradingStatus = null } = values;
|
|
const share = values.instrument;
|
|
const priceAt = timestamp(last?.time);
|
|
const currency = share?.currency?.toUpperCase();
|
|
const price =
|
|
last?.price && currency
|
|
? { currency, value: formatDecimal(decimal(last.price)) }
|
|
: null;
|
|
const previousPrice =
|
|
previous?.close && currency
|
|
? { currency, value: formatDecimal(decimal(previous.close)) }
|
|
: null;
|
|
const difference =
|
|
price && previousPrice
|
|
? subtract(
|
|
decimalFromString(price.value),
|
|
decimalFromString(previousPrice.value),
|
|
)
|
|
: null;
|
|
return {
|
|
price,
|
|
priceAt,
|
|
tradingStatus,
|
|
previousCloseDate: timestamp(previous?.time)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null,
|
|
previousClose: previousPrice,
|
|
change:
|
|
difference && currency
|
|
? { currency, value: formatDecimal(difference) }
|
|
: null,
|
|
changePercent:
|
|
difference &&
|
|
previousPrice &&
|
|
decimalFromString(previousPrice.value).value > ZERO_AMOUNT
|
|
? formatDecimal(
|
|
divide(
|
|
multiply(difference, decimalFromString('100')),
|
|
decimalFromString(previousPrice.value),
|
|
CHANGE_PERCENT_DECIMAL_PLACES,
|
|
),
|
|
)
|
|
: null,
|
|
unavailable,
|
|
observedAt,
|
|
};
|
|
}
|