feat(performance): investment performance vs Moscow Exchange index (WealthLedger/main#54) (#200)
Backend verify / commits (push) Skipped
Backend verify / security (push) Successful in 1m7s
Backend verify / verify (push) Failing after 7m19s

This commit was merged in pull request #200.
This commit is contained in:
2026-11-08 16:45:29 +03:00
parent 84433ee207
commit fee61537b6
28 changed files with 1507 additions and 4 deletions
@@ -0,0 +1,35 @@
import { uncoveredOperationIntervals } from '../../operations/domain/operations-sync-plan';
const ZERO_MILLIS = 0;
const NEXT_MILLISECOND = 1;
export enum PerformanceUnavailableReason {
AccountValuationDatesDiffer = 'ACCOUNT_VALUATION_DATES_DIFFER',
AmbiguousReturn = 'AMBIGUOUS_RETURN',
HistoryStartUnknown = 'HISTORY_START_UNKNOWN',
IndexHistoryUnavailable = 'INDEX_HISTORY_UNAVAILABLE',
NonRubActivity = 'NON_RUB_ACTIVITY',
NoSolution = 'NO_SOLUTION',
OperationsHistoryIncomplete = 'OPERATIONS_HISTORY_INCOMPLETE',
PortfolioValuationIncomplete = 'PORTFOLIO_VALUATION_INCOMPLETE',
UnconfirmedTransfer = 'UNCONFIRMED_TRANSFER',
UnvaluedSecuritiesTransfer = 'UNVALUED_SECURITIES_TRANSFER',
}
export const performanceUnavailableReasons = Object.values(PerformanceUnavailableReason);
type Coverage = { asOf: Date; from: Date; to: Date };
/** Coverage is half-open and must include the instant of the last valuation. */
export function hasPerformanceCoverage(
accountId: string,
from: Date,
valuationAt: Date,
coverage: Coverage[],
): boolean {
if (from.valueOf() >= valuationAt.valueOf()) return false;
const end = new Date(valuationAt.valueOf() + NEXT_MILLISECOND);
if (!coverage.some((item) => item.asOf >= valuationAt && item.to > valuationAt)) return false;
const confirmed = coverage.map((item) => ({ accountId, from: item.from, to: item.to }));
return uncoveredOperationIntervals([{ accountId, from, to: end }], confirmed).length === ZERO_MILLIS;
}