refactor: organize backend by business responsibility
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { ServiceUnavailableException } from '@nestjs/common';
|
||||
import type {
|
||||
TbankCandle,
|
||||
TbankTimestamp,
|
||||
} from '../../../../integrations/tbank/tbank.api';
|
||||
export function timestamp(value?: TbankTimestamp): string | null {
|
||||
const seconds = Number(value?.seconds);
|
||||
return Number.isFinite(seconds) && seconds > 0
|
||||
? new Date(seconds * 1000).toISOString()
|
||||
: null;
|
||||
}
|
||||
export function moscowDay(iso: string): string {
|
||||
return new Date(Date.parse(iso) + 3 * 3_600_000).toISOString().slice(0, 10);
|
||||
}
|
||||
export function previousClose(
|
||||
candles: TbankCandle[],
|
||||
priceAt: string,
|
||||
): TbankCandle | null {
|
||||
return (
|
||||
candles
|
||||
.filter((candle) => {
|
||||
const time = timestamp(candle.time);
|
||||
return (
|
||||
candle.isComplete &&
|
||||
candle.close &&
|
||||
time &&
|
||||
time.slice(0, 10) < moscowDay(priceAt)
|
||||
);
|
||||
})
|
||||
.sort((a, b) => Number(b.time?.seconds) - Number(a.time?.seconds))[0] ??
|
||||
null
|
||||
);
|
||||
}
|
||||
export async function optional<T>(
|
||||
operation: () => Promise<T>,
|
||||
name: string,
|
||||
unavailable: string[],
|
||||
): Promise<T | null> {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
if (!(error instanceof ServiceUnavailableException)) throw error;
|
||||
unavailable.push(name);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
add,
|
||||
decimalFromString,
|
||||
divide,
|
||||
formatDecimal,
|
||||
multiply,
|
||||
} from '../../../../common/decimal';
|
||||
|
||||
import { MoneyDto } from '../../../../common/money.dto';
|
||||
import {
|
||||
PortfolioDto,
|
||||
PortfolioPositionDto,
|
||||
} from '../../portfolio/portfolio.api';
|
||||
import { StockPositionDto } from '../stocks/presentation/stock.dto';
|
||||
|
||||
export function instrumentPosition(
|
||||
portfolio: PortfolioDto,
|
||||
uid: string,
|
||||
type: 'STOCK' | 'BOND',
|
||||
): StockPositionDto {
|
||||
const positions = portfolio.positions.filter(
|
||||
(p) => p.type === type && p.instrumentUid === uid,
|
||||
);
|
||||
const zero = decimalFromString('0');
|
||||
const quantity = positions
|
||||
.map((p) => decimalFromString(p.quantity))
|
||||
.reduce(add, zero);
|
||||
const costs = positions.map(cost);
|
||||
const totalCost = sumComplete(costs);
|
||||
const currentValue = sumComplete(positions.map((p) => p.currentValue));
|
||||
const absoluteReturn = sumComplete(positions.map((p) => p.absoluteReturn));
|
||||
const positive =
|
||||
positions.length > 0 &&
|
||||
positions.every((p) => decimalFromString(p.quantity).value > 0n);
|
||||
return {
|
||||
quantity: formatDecimal(quantity),
|
||||
currentValue,
|
||||
absoluteReturn,
|
||||
averagePurchasePrice:
|
||||
positive && totalCost
|
||||
? {
|
||||
currency: totalCost.currency,
|
||||
value: formatDecimal(
|
||||
divide(decimalFromString(totalCost.value), quantity, 9),
|
||||
),
|
||||
}
|
||||
: null,
|
||||
returnPercent: positive ? percent(absoluteReturn, totalCost) : null,
|
||||
accounts: positions.map((p) => ({
|
||||
...p,
|
||||
returnPercent:
|
||||
decimalFromString(p.quantity).value > 0n
|
||||
? percent(p.absoluteReturn, cost(p))
|
||||
: null,
|
||||
})),
|
||||
isStale: portfolio.isStale,
|
||||
observedAt: portfolio.lastSuccessfulAt,
|
||||
};
|
||||
}
|
||||
function cost(position: PortfolioPositionDto): MoneyDto | null {
|
||||
const price = position.averagePurchasePrice;
|
||||
return price
|
||||
? {
|
||||
currency: price.currency,
|
||||
value: formatDecimal(
|
||||
multiply(
|
||||
decimalFromString(price.value),
|
||||
decimalFromString(position.quantity),
|
||||
),
|
||||
),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
function sumComplete(values: (MoneyDto | null)[]): MoneyDto | null {
|
||||
const currency = values[0]?.currency;
|
||||
if (
|
||||
!currency ||
|
||||
!values.every((v): v is MoneyDto => v !== null && v.currency === currency)
|
||||
)
|
||||
return null;
|
||||
return {
|
||||
currency,
|
||||
value: formatDecimal(
|
||||
values.reduce(
|
||||
(sum, value) => add(sum, decimalFromString(value.value)),
|
||||
decimalFromString('0'),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
function percent(
|
||||
result: MoneyDto | null,
|
||||
base: MoneyDto | null,
|
||||
): string | null {
|
||||
if (
|
||||
!base ||
|
||||
!result ||
|
||||
base.currency !== result.currency ||
|
||||
decimalFromString(base.value).value <= 0n
|
||||
)
|
||||
return null;
|
||||
return formatDecimal(
|
||||
divide(
|
||||
multiply(decimalFromString(result.value), decimalFromString('100')),
|
||||
decimalFromString(base.value),
|
||||
6,
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user