refactor: centralize domain values and enforce numeric lint
This commit is contained in:
@@ -15,6 +15,8 @@ import { decimal } from '../../../../../integrations/tbank/decimal';
|
||||
import { optional, timestamp } from '../../application/instrument-market';
|
||||
import { StockDto, StockSearchResultDto } from '../presentation/stock.dto';
|
||||
|
||||
const SEARCH_RESULTS_LIMIT = 30;
|
||||
|
||||
type IdentifiedShare = TbankShare & { uid: string };
|
||||
|
||||
@Injectable()
|
||||
@@ -30,7 +32,7 @@ export class StockCatalogService {
|
||||
search(userId: string, query: string): Promise<StockSearchResultDto[]> {
|
||||
return this.cached(userId, `search:${query}`, async (token) => {
|
||||
const found = await this.tbank.findShares(token, query);
|
||||
const shares = selectShares(found, query).slice(0, 30);
|
||||
const shares = selectShares(found, query).slice(0, SEARCH_RESULTS_LIMIT);
|
||||
if (!shares.length) return [];
|
||||
// One market-data request for the list; failed enrichment must not hide a catalog result.
|
||||
const [details, prices] = await Promise.all([
|
||||
|
||||
@@ -20,6 +20,9 @@ import {
|
||||
} from '../presentation/stock.dto';
|
||||
|
||||
import { StockCatalogService } from './stock-catalog.service';
|
||||
|
||||
const ISO_YEAR_LENGTH = 4;
|
||||
const INCOME_HISTORY_YEARS = 5;
|
||||
@Injectable()
|
||||
export class StockIncomeService {
|
||||
constructor(
|
||||
@@ -36,8 +39,8 @@ export class StockIncomeService {
|
||||
return this.catalog.cached(userId, `dividends:${uid}`, async (token) => {
|
||||
await this.catalog.share(token, uid);
|
||||
const today = moscowDay(new Date().toISOString());
|
||||
const from = `${Number(today.slice(0, 4)) - 5}${today.slice(4)}`;
|
||||
const to = `${Number(today.slice(0, 4)) + 1}${today.slice(4)}`;
|
||||
const from = `${Number(today.slice(0, ISO_YEAR_LENGTH)) - INCOME_HISTORY_YEARS}${today.slice(ISO_YEAR_LENGTH)}`;
|
||||
const to = `${Number(today.slice(0, ISO_YEAR_LENGTH)) + 1}${today.slice(ISO_YEAR_LENGTH)}`;
|
||||
const unavailable: string[] = [];
|
||||
const [events, position] = await Promise.all([
|
||||
this.tbank.getDividends(token, uid, from, to),
|
||||
|
||||
@@ -22,6 +22,12 @@ import {
|
||||
const dayMs = 86_400_000;
|
||||
|
||||
import { StockCatalogService } from './stock-catalog.service';
|
||||
|
||||
const PREVIOUS_CLOSE_LOOKBACK_DAYS = 20;
|
||||
const HALF_YEAR_MONTHS = 6;
|
||||
const YEAR_MONTHS = 12;
|
||||
const CANDLE_WINDOW_DAYS = 365;
|
||||
const CHANGE_PERCENT_DECIMAL_PLACES = 6;
|
||||
@Injectable()
|
||||
export class StockMarketService {
|
||||
constructor(
|
||||
@@ -58,7 +64,9 @@ export class StockMarketService {
|
||||
await this.tbank.getCandles(
|
||||
token,
|
||||
uid,
|
||||
new Date(end.valueOf() - 20 * dayMs),
|
||||
new Date(
|
||||
end.valueOf() - PREVIOUS_CLOSE_LOOKBACK_DAYS * dayMs,
|
||||
),
|
||||
end,
|
||||
),
|
||||
priceAt,
|
||||
@@ -97,7 +105,7 @@ export class StockMarketService {
|
||||
divide(
|
||||
multiply(difference, decimalFromString('100')),
|
||||
decimalFromString(previousPrice.value),
|
||||
6,
|
||||
CHANGE_PERCENT_DECIMAL_PLACES,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
@@ -121,7 +129,11 @@ export class StockMarketService {
|
||||
const from = new Date(to);
|
||||
from.setUTCMonth(
|
||||
from.getUTCMonth() -
|
||||
(period === 'month' ? 1 : period === 'halfYear' ? 6 : 12),
|
||||
(period === 'month'
|
||||
? 1
|
||||
: period === 'halfYear'
|
||||
? HALF_YEAR_MONTHS
|
||||
: YEAR_MONTHS),
|
||||
);
|
||||
if (period === 'all') {
|
||||
if (!historyFrom)
|
||||
@@ -134,7 +146,10 @@ export class StockMarketService {
|
||||
const collected = new Map<string, StockCandlesDto['candles'][number]>();
|
||||
// Conservative one-year windows also work with older deployed RPC limits.
|
||||
for (let start = from.valueOf(); start < to.valueOf();) {
|
||||
const end = Math.min(start + 365 * dayMs, to.valueOf());
|
||||
const end = Math.min(
|
||||
start + CANDLE_WINDOW_DAYS * dayMs,
|
||||
to.valueOf(),
|
||||
);
|
||||
for (const candle of await this.tbank.getCandles(
|
||||
token,
|
||||
uid,
|
||||
|
||||
@@ -11,25 +11,27 @@ import {
|
||||
import { MoneyDto } from '../../../../../common/money.dto';
|
||||
import { PortfolioPositionDto } from '../../../portfolio/portfolio.api';
|
||||
|
||||
const MAX_INSTRUMENT_QUERY_LENGTH = 80;
|
||||
|
||||
export class StockSearchQueryDto {
|
||||
@ApiProperty({ minLength: 2, maxLength: 80 })
|
||||
@ApiProperty({ minLength: 2, maxLength: MAX_INSTRUMENT_QUERY_LENGTH })
|
||||
@Transform(({ value }: { value: unknown }) =>
|
||||
typeof value === 'string' ? value.trim() : value,
|
||||
)
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(80)
|
||||
@MaxLength(MAX_INSTRUMENT_QUERY_LENGTH)
|
||||
declare query: string;
|
||||
}
|
||||
export class StockTickerParamsDto {
|
||||
@ApiProperty({
|
||||
minLength: 1,
|
||||
maxLength: 80,
|
||||
maxLength: MAX_INSTRUMENT_QUERY_LENGTH,
|
||||
pattern: '^[A-Za-z0-9][A-Za-z0-9._@+-]*$',
|
||||
})
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(80)
|
||||
@MaxLength(MAX_INSTRUMENT_QUERY_LENGTH)
|
||||
@Matches(/^[A-Za-z0-9][A-Za-z0-9._@+-]*$/)
|
||||
declare ticker: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user