Compare commits
51
Commits
v0.1.1
...
3c9efcba03
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c9efcba03 | ||
|
|
e1981a24bf | ||
|
|
364f6bd8d0 | ||
|
|
6d7d4c7b28 | ||
|
|
40ea47a7c2 | ||
|
|
37430da4c9 | ||
|
|
abe5b16241 | ||
|
|
dcb528fa9f | ||
|
|
8c6c2f84ce | ||
|
|
f9098d8409 | ||
|
|
4216eb815f | ||
|
|
24ad250c02 | ||
|
|
e8e3ef6285 | ||
|
|
11f819ed31 | ||
|
|
550143aea1 | ||
|
|
56885f450b | ||
|
|
1c89d321b1 | ||
|
|
ad1978c75e | ||
|
|
77c1f44726 | ||
|
|
8a2a62c611 | ||
|
|
7b61eb8e6b | ||
|
|
cad0f31615 | ||
|
|
03110e99ea | ||
|
|
03fb045a9e | ||
|
|
add9e5f638 | ||
|
|
a818889e54 | ||
|
|
803f0843f8 | ||
|
|
1378ac9fe2 | ||
|
|
04dc662b2e | ||
|
|
c24e4b990a | ||
|
|
602eb43294 | ||
|
|
eaf7ede841 | ||
|
|
fe99b05e93 | ||
|
|
e6fd4f64b8 | ||
|
|
4caa5e92b7 | ||
|
|
2c34023af8 | ||
|
|
f0cab445d7 | ||
|
|
df6f0109ed | ||
|
|
0f8bbee10a | ||
|
|
e0bceea936 | ||
|
|
9a9b0409cc | ||
|
|
256d49b045 | ||
|
|
9477e688d1 | ||
|
|
0ee54ca952 | ||
|
|
6fa62fb8b5 | ||
|
|
e4838e1485 | ||
|
|
07de062ebb | ||
|
|
55610ea2b4 | ||
|
|
15d1d820f5 | ||
|
|
6fd39cd467 | ||
|
|
5de85c296f |
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Неверное поведение
|
||||||
|
---
|
||||||
|
# Bug Report
|
||||||
|
|
||||||
|
## Проблема и шаги воспроизведения
|
||||||
|
|
||||||
|
## Факт / ожидание
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## Окружение / ссылки (если применимо)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
name: Engineering Task
|
||||||
|
about: CI, тесты, документация, процесс и прочее
|
||||||
|
---
|
||||||
|
# Engineering Task
|
||||||
|
|
||||||
|
## Контекст и результат
|
||||||
|
|
||||||
|
## Scope / связанные Issues (если есть)
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Новое наблюдаемое поведение
|
||||||
|
---
|
||||||
|
# Feature Request
|
||||||
|
|
||||||
|
## Зачем и какой результат
|
||||||
|
|
||||||
|
## Ожидаемое поведение
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## Зависимости / связанные Issues (если есть)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
name: Refactoring / Technical Debt
|
||||||
|
about: Улучшение без нового поведения
|
||||||
|
---
|
||||||
|
# Refactoring / Technical Debt
|
||||||
|
|
||||||
|
## Проблема и результат
|
||||||
|
|
||||||
|
## Сохраняемые инварианты / границы
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## Зависимости / ссылки (если есть)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Pull Request
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Краткий результат работы описывается здесь.
|
||||||
|
|
||||||
|
## Linked Issue
|
||||||
|
|
||||||
|
Refs `<owner/repo#number>` (заменить ссылкой на реальную Issue).
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
Затронутые части и изменения.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Команда / результат / ограничения:
|
||||||
|
|
||||||
|
## Breaking Changes (если есть)
|
||||||
@@ -4,6 +4,10 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened, edited]
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: docs-commits-${{ gitea.event.pull_request.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
commits:
|
commits:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -23,4 +27,8 @@ jobs:
|
|||||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
run: |
|
run: |
|
||||||
printf '%s\n' "$PR_TITLE" | npm run commitlint -- --strict
|
printf '%s\n' "$PR_TITLE" | npm run commitlint -- --strict
|
||||||
npm run commitlint -- --strict --from "$BASE_SHA" --to "$HEAD_SHA"
|
if [[ "$PR_TITLE" =~ ^chore\(release\):\ [0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
npm run commitlint -- --strict --from "$HEAD_SHA^" --to "$HEAD_SHA"
|
||||||
|
else
|
||||||
|
npm run commitlint -- --strict --from "$BASE_SHA" --to "$HEAD_SHA"
|
||||||
|
fi
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ name: Docs security
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [develop, main]
|
||||||
- '**'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '17 5 * * *'
|
- cron: '17 5 * * *'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: docs-security-${{ gitea.event_name }}-${{ gitea.event.pull_request.number || gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
security:
|
security:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -2,8 +2,13 @@ name: Docs verify
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['**']
|
branches: [develop, main]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: docs-verify-${{ gitea.event_name }}-${{ gitea.event.pull_request.number || gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
verify:
|
verify:
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": ".markdownlint.json",
|
||||||
|
"MD024": {
|
||||||
|
"siblings_only": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"MD013": false
|
|
||||||
},
|
|
||||||
"customRules": [
|
|
||||||
"markdownlint-rule-relative-links"
|
|
||||||
],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"filter": [
|
|
||||||
"CHANGELOG.md"
|
|
||||||
],
|
|
||||||
"config": {
|
|
||||||
"MD024": {
|
|
||||||
"siblings_only": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"combine": "merge"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
module.exports = require('markdownlint-rule-relative-links').default;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"MD013": false
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@
|
|||||||
Перед изменением прочитай [README](README.md) и [CONTRIBUTING](CONTRIBUTING.md),
|
Перед изменением прочитай [README](README.md) и [CONTRIBUTING](CONTRIBUTING.md),
|
||||||
затем только затронутую тему. Docs объясняет систему; требования принадлежат
|
затем только затронутую тему. Docs объясняет систему; требования принадлежат
|
||||||
основным спецификациям, реализация и coding conventions — соседним репозиториям.
|
основным спецификациям, реализация и coding conventions — соседним репозиториям.
|
||||||
|
До edits найди/создай подходящую Gitea Issue, проверь AC и ветку; чтение
|
||||||
|
допускается без Issue. Общий маршрут — [Engineering Workflow](https://git.personal.ksv741.ru/WealthLedger/main/src/commit/03b3b59e6ff2ba391f9dfc42a039a96d0d84c1c9/.ai/conventions/engineering-workflow.md)
|
||||||
|
(main#28; после merge — актуальная версия `develop`).
|
||||||
|
|
||||||
Проверь существенные факты по локальным тематическим страницам и ADR;
|
Проверь существенные факты по локальным тематическим страницам и ADR;
|
||||||
внешние источники используй как справочные свидетельства, когда они доступны.
|
внешние источники используй как справочные свидетельства, когда они доступны.
|
||||||
@@ -20,7 +23,13 @@
|
|||||||
|
|
||||||
## Ветки и релизы
|
## Ветки и релизы
|
||||||
|
|
||||||
Работа — в отдельной ветке от `develop`, затем PR в `develop`; в `main` напрямую
|
Работа — в issue-linked ветке от `develop`, затем PR в `develop`; в `main` напрямую
|
||||||
не пушить. Release/hotfix и синхронизация веток — по [README](README.md#релизы).
|
не пушить. PR сливается squash или rebase-merge; merge-коммит не используется.
|
||||||
Commits и заголовки PR — Conventional Commits; для спецификаций допустим `spec:`.
|
`main` обновляется только fast-forward из `develop`, поэтому `origin/main`
|
||||||
|
всегда предок `origin/develop`:
|
||||||
|
`git merge-base --is-ancestor origin/main origin/develop`. Срочный fix —
|
||||||
|
обычная ветка `fix/*` от `develop`; пути hotfix от `main` нет.
|
||||||
|
Релиз — по [README](README.md#релизы).
|
||||||
|
Commits и заголовки PR — Conventional Commits со ссылкой на Issue (кроме release);
|
||||||
|
для спецификаций допустим `spec:`.
|
||||||
Перед подготовкой релиза прочитай местный README; версии вручную не повышать.
|
Перед подготовкой релиза прочитай местный README; версии вручную не повышать.
|
||||||
|
|||||||
@@ -1,5 +1,94 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.1.8](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.7...v0.1.8) (2026-11-06)
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
* **deps:** set Renovate release age, PR limit and update groups (WealthLedger/main[#6](https://git.personal.ksv741.ru/WealthLedger/docs/issues/6)) ([#64](https://git.personal.ksv741.ru/WealthLedger/docs/issues/64)) ([6d7d4c7](https://git.personal.ksv741.ru/WealthLedger/docs/commit/6d7d4c7b286535b7338d7e398d2daf7d8137a501))
|
||||||
|
|
||||||
|
## [0.1.7](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.6...v0.1.7) (2026-11-01)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
* describe all-account row grouping (WealthLedger/main[#47](https://git.personal.ksv741.ru/WealthLedger/docs/issues/47)) ([#59](https://git.personal.ksv741.ru/WealthLedger/docs/issues/59)) ([dcb528f](https://git.personal.ksv741.ru/WealthLedger/docs/commit/dcb528fa9f76f0d7aa98bd827cc7ee39024dbb13))
|
||||||
|
* describe concentration tab and stored instrument classification (WealthLedger/main[#48](https://git.personal.ksv741.ru/WealthLedger/docs/issues/48)) ([#60](https://git.personal.ksv741.ru/WealthLedger/docs/issues/60)) ([37430da](https://git.personal.ksv741.ru/WealthLedger/docs/commit/37430da4c99cb571ef5bc0676cad7b3a47bcb6f0))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
* **deps:** resolve markdownlint audit advisory (WealthLedger/main[#49](https://git.personal.ksv741.ru/WealthLedger/docs/issues/49)) ([#61](https://git.personal.ksv741.ru/WealthLedger/docs/issues/61)) ([abe5b16](https://git.personal.ksv741.ru/WealthLedger/docs/commit/abe5b16241d3002baa5d39c2a12949f23a75b968))
|
||||||
|
|
||||||
|
## [0.1.6](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.5...v0.1.6) (2026-10-24)
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
* bump Node to 24.22.1 (WealthLedger/backend[#122](https://git.personal.ksv741.ru/WealthLedger/docs/issues/122)) ([4216eb8](https://git.personal.ksv741.ru/WealthLedger/docs/commit/4216eb815f59b7a284e679ccb1b725438da07882))
|
||||||
|
* **deps:** update non-major dependencies (WealthLedger/docs[#56](https://git.personal.ksv741.ru/WealthLedger/docs/issues/56)) ([f9098d8](https://git.personal.ksv741.ru/WealthLedger/docs/commit/f9098d84098467e41143bad5044085d6329099d9))
|
||||||
|
|
||||||
|
## [0.1.5](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.4...v0.1.5) (2026-10-18)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
* align current pages with hourly freshness model (WealthLedger/docs[#34](https://git.personal.ksv741.ru/WealthLedger/docs/issues/34)) ([#42](https://git.personal.ksv741.ru/WealthLedger/docs/issues/42)) ([7b61eb8](https://git.personal.ksv741.ru/WealthLedger/docs/commit/7b61eb8e6ba93a765b1ae27ca31d77b5e422e149))
|
||||||
|
* describe ASCII 8-72 password policy and drop resolved discrepancy (WealthLedger/docs[#46](https://git.personal.ksv741.ru/WealthLedger/docs/issues/46)) ([#47](https://git.personal.ksv741.ru/WealthLedger/docs/issues/47)) ([56885f4](https://git.personal.ksv741.ru/WealthLedger/docs/commit/56885f450bc1298d383d496174d16e591926e047))
|
||||||
|
* describe events default period and picker bounds (WealthLedger/frontend[#63](https://git.personal.ksv741.ru/WealthLedger/docs/issues/63)) ([8a2a62c](https://git.personal.ksv741.ru/WealthLedger/docs/commit/8a2a62c611e1acc1c1e69048a88b049bbf0d9087))
|
||||||
|
* describe quiet reading and visible source state ([#51](https://git.personal.ksv741.ru/WealthLedger/docs/issues/51)) ([e8e3ef6](https://git.personal.ksv741.ru/WealthLedger/docs/commit/e8e3ef62854dae8448deaa66ac26e5b15d5c7d6a))
|
||||||
|
* document income without comparisons and unified payouts (WealthLedger/main[#33](https://git.personal.ksv741.ru/WealthLedger/docs/issues/33)) ([#40](https://git.personal.ksv741.ru/WealthLedger/docs/issues/40)) ([03fb045](https://git.personal.ksv741.ru/WealthLedger/docs/commit/03fb045a9ee8a36775599bda66e7f6a08362bd84))
|
||||||
|
* document Nexus delivery with shared account (WealthLedger/docs[#18](https://git.personal.ksv741.ru/WealthLedger/docs/issues/18)) ([#48](https://git.personal.ksv741.ru/WealthLedger/docs/issues/48)) ([550143a](https://git.personal.ksv741.ru/WealthLedger/docs/commit/550143aea1f0ac90630f326dd927091b0ec4f38e))
|
||||||
|
|
||||||
|
### CI
|
||||||
|
|
||||||
|
* rerun commits proof on PR title edit (WealthLedger/docs[#35](https://git.personal.ksv741.ru/WealthLedger/docs/issues/35)) ([03110e9](https://git.personal.ksv741.ru/WealthLedger/docs/commit/03110e99ea58600bacd4c091fda8591d374a5a8d))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
* adopt linear git model in docs instructions ([#49](https://git.personal.ksv741.ru/WealthLedger/docs/issues/49)) ([11f819e](https://git.personal.ksv741.ru/WealthLedger/docs/commit/11f819ed31226a05524ac35a893064525713e41b))
|
||||||
|
|
||||||
|
## [0.1.4](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.3...v0.1.4) (2026-10-12)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* align docs commitlint workflow (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/docs/issues/27)) ([602eb43](https://git.personal.ksv741.ru/WealthLedger/docs/commit/602eb43294509274cbd9003be31f726b6e275a1e))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
* clarify card summary totals ([df6f010](https://git.personal.ksv741.ru/WealthLedger/docs/commit/df6f0109ed696a529cf2622e23fceeab8133f932))
|
||||||
|
* document hourly attempt lifecycle semantics (WealthLedger/backend[#56](https://git.personal.ksv741.ru/WealthLedger/docs/issues/56)) ([1378ac9](https://git.personal.ksv741.ru/WealthLedger/docs/commit/1378ac9fe2fcfd43bd71421e62b02f00069be928))
|
||||||
|
* document hourly investment reads ([f0cab44](https://git.personal.ksv741.ru/WealthLedger/docs/commit/f0cab445d7e7076bf92a61f1023b8bf51e094d33))
|
||||||
|
* document hourly sync history ([e0bceea](https://git.personal.ksv741.ru/WealthLedger/docs/commit/e0bceea936239c08f4717e264be23d7f60552398))
|
||||||
|
* document stock screener ([2c34023](https://git.personal.ksv741.ru/WealthLedger/docs/commit/2c34023af8593f305f7c81e672b4e179c0992cf3))
|
||||||
|
* pin docs workflow reference (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/docs/issues/27)) ([c24e4b9](https://git.personal.ksv741.ru/WealthLedger/docs/commit/c24e4b990aaeffd02e05f7de21942579e452cecf))
|
||||||
|
|
||||||
|
### CI
|
||||||
|
|
||||||
|
* avoid duplicate PR checks ([4caa5e9](https://git.personal.ksv741.ru/WealthLedger/docs/commit/4caa5e92b7d2597e5c7725b2036007b71ece2abd))
|
||||||
|
* skip checks on PR metadata edits ([e6fd4f6](https://git.personal.ksv741.ru/WealthLedger/docs/commit/e6fd4f64b81dd73b33959b845a1babcfac1891c8))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
* align docs engineering workflow (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/docs/issues/27)) ([eaf7ede](https://git.personal.ksv741.ru/WealthLedger/docs/commit/eaf7ede8414f0df972f9ae7ab9e223a6350b5462))
|
||||||
|
|
||||||
|
## [0.1.3](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.2...v0.1.3) (2026-10-07)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deps:** refresh docs security baseline ([256d49b](https://git.personal.ksv741.ru/WealthLedger/docs/commit/256d49b04591d55d980a22c49b413432c9bda746))
|
||||||
|
* **deps:** remove vulnerable docs tooling ([9477e68](https://git.personal.ksv741.ru/WealthLedger/docs/commit/9477e688d180783881abc92f8835517715880279))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
* record code quality review and refactoring ([6fa62fb](https://git.personal.ksv741.ru/WealthLedger/docs/commit/6fa62fb8b51aba82766fb50b3e5871572af8be27))
|
||||||
|
* record minimal runtime coordination ([0ee54ca](https://git.personal.ksv741.ru/WealthLedger/docs/commit/0ee54ca952db6ca7264c0d7fe8cf5759b3187c5b))
|
||||||
|
|
||||||
|
## [0.1.2](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.1...v0.1.2) (2026-09-26)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
* describe local frontend packages and ownership ([55610ea](https://git.personal.ksv741.ru/WealthLedger/docs/commit/55610ea2b4dc687f76ee9eb37c14d887b1b70100))
|
||||||
|
* document Nexus registry migration [#18](https://git.personal.ksv741.ru/WealthLedger/docs/issues/18) ([5de85c2](https://git.personal.ksv741.ru/WealthLedger/docs/commit/5de85c296f7681882f58476343d3da71f1dee28d))
|
||||||
|
* document versioned deployment bundle ([15d1d82](https://git.personal.ksv741.ru/WealthLedger/docs/commit/15d1d820f5401c25adda23f753b99e894bdcd4d3))
|
||||||
|
* explain persisted T-Bank reads and synchronization ([07de062](https://git.personal.ksv741.ru/WealthLedger/docs/commit/07de062ebbf043eff50bd7f53b48bff13bb7cb1b))
|
||||||
|
* use shared Nexus account [#18](https://git.personal.ksv741.ru/WealthLedger/docs/issues/18) ([6fd39cd](https://git.personal.ksv741.ru/WealthLedger/docs/commit/6fd39cd467566e23cca79a1235125dfb80fa99f7))
|
||||||
|
|
||||||
## [0.1.1](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.0...v0.1.1) (2026-09-16)
|
## [0.1.1](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.0...v0.1.1) (2026-09-16)
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|||||||
+4
-3
@@ -177,9 +177,10 @@ Markdown-only правки не требуют тестов приложения
|
|||||||
|
|
||||||
`security.yml` при push, PR и ежедневно проверяет всю Git-историю через
|
`security.yml` при push, PR и ежедневно проверяет всю Git-историю через
|
||||||
Gitleaks 8.30.1 с redacted-выводом и все npm dependencies через
|
Gitleaks 8.30.1 с redacted-выводом и все npm dependencies через
|
||||||
`npm audit --include=dev --audit-level=low`. Override `smol-toml` исправляет
|
`npm audit --include=dev --audit-level=low`. Overrides `smol-toml` и `js-yaml`
|
||||||
DoS в TOML parser markdownlint-cli2; его можно удалить, когда родительский
|
исправляют DoS в parsers markdownlint-cli; overrides для release-it обновляют
|
||||||
пакет примет исправленную версию.
|
уязвимые basic-ftp и undici. Их можно удалить, когда родительские пакеты
|
||||||
|
примут исправленные версии.
|
||||||
|
|
||||||
Renovate объединяет minor/patch updates; major требует согласования в
|
Renovate объединяет minor/patch updates; major требует согласования в
|
||||||
Dependency Dashboard, automerge не включён. Required checks включаются
|
Dependency Dashboard, automerge не включён. Required checks включаются
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ WealthLedger помогает частному инвестору понимат
|
|||||||
| [Аутентификация](domains/authentication/overview.md) | Регистрация, вход, восстановление и завершение сессии |
|
| [Аутентификация](domains/authentication/overview.md) | Регистрация, вход, восстановление и завершение сессии |
|
||||||
| [Счета](domains/accounts/overview.md) | Доступные счета, сводки и состояния каждого счёта |
|
| [Счета](domains/accounts/overview.md) | Доступные счета, сводки и состояния каждого счёта |
|
||||||
| [Банковские карты](domains/cards/overview.md) | Ручные остатки, операции, категории, возвраты, месячные сводки и исправления |
|
| [Банковские карты](domains/cards/overview.md) | Ручные остатки, операции, категории, возвраты, месячные сводки и исправления |
|
||||||
| [Портфель](domains/portfolio/overview.md) | Позиции, остатки, суммы, структура и свежесть |
|
| [Портфель](domains/portfolio/overview.md) | Позиции, остатки, суммы, структура, [концентрация](domains/portfolio/concentration.md) и свежесть |
|
||||||
| [Все счета](domains/portfolio/all-accounts.md) | Общий портфель, происхождение строк и сводные разделы |
|
| [Все счета](domains/portfolio/all-accounts.md) | Общий портфель, происхождение строк и сводные разделы |
|
||||||
| [Операции](domains/operations/overview.md) | Исполненные операции, фильтры и пагинация |
|
| [Операции](domains/operations/overview.md) | Исполненные операции, фильтры и пагинация |
|
||||||
| [Доход](domains/income/overview.md) | Полученные купоны и дивиденды, периоды и состав расчёта |
|
| [Доход](domains/income/overview.md) | Полученные купоны и дивиденды, периоды и состав расчёта |
|
||||||
| [Будущие выплаты](domains/income/upcoming-events.md) | Календарь по текущим позициям и ограничения прогноза |
|
| [Будущие выплаты](domains/income/upcoming-events.md) | Календарь по текущим позициям и ограничения прогноза |
|
||||||
| [Акции](domains/instruments/stocks.md) и [облигации](domains/instruments/bonds.md) | Поиск выпуска, рыночные показатели и личная история |
|
| [Скринер акций](domains/instruments/stocks.md) и [облигации](domains/instruments/bonds.md) | Подбор акций по параметрам, поиск выпуска, рыночные показатели и личная история |
|
||||||
|
|
||||||
## Найти материал по задаче
|
## Найти материал по задаче
|
||||||
|
|
||||||
@@ -72,35 +72,60 @@ WealthLedger помогает частному инвестору понимат
|
|||||||
## Релизы
|
## Релизы
|
||||||
|
|
||||||
Репозиторий выпускается независимо. `develop` — интеграция, `main` — принятые
|
Репозиторий выпускается независимо. `develop` — интеграция, `main` — принятые
|
||||||
релизы. Работа: `codex/<task>` от актуального `develop`, PR в `develop`.
|
релизы. Обычная работа: issue-linked ветка от актуального `develop`, PR в `develop`;
|
||||||
Обычные PR допускают squash; release и обратная синхронизация сохраняют историю.
|
общие правила — в Engineering Workflow по ссылке из AGENTS.
|
||||||
Commits и PR titles — Conventional Commits, включая `spec:`. Версии вручную не повышай.
|
PR сливается squash или rebase-merge; merge-коммит не используется. `main`
|
||||||
|
обновляется только fast-forward из `develop`, поэтому история линейна:
|
||||||
|
|
||||||
1. Получи `main`, `develop` и теги: `git fetch origin --tags`. Убедись, что
|
```text
|
||||||
`develop` содержит последний релиз. Создай `release/<name>` от проверенного
|
<type>/<issue>-<slug> --squash--> develop --ff-only--> main --> tag vX.Y.Z
|
||||||
`develop`; `nvm use --silent`, `npm ci`. Hotfix начинается от `main` в
|
```
|
||||||
`release/hotfix-<name>` и проходит тот же процесс.
|
|
||||||
|
Инвариант: `origin/main` всегда предок `origin/develop`. Проверка:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch -q origin
|
||||||
|
git merge-base --is-ancestor origin/main origin/develop && echo ok || echo BROKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
Commits и PR titles — Conventional Commits со ссылкой на Issue (кроме release),
|
||||||
|
включая `spec:`. Версии вручную не повышай.
|
||||||
|
|
||||||
|
1. Получи `main`, `develop` и теги: `git fetch origin --tags`. Проверь
|
||||||
|
инвариант. Создай `release/X.Y.Z` от проверенного `develop`;
|
||||||
|
`nvm use --silent`, `npm ci`.
|
||||||
2. На чистой ветке выполни `npm run release:preview`, просмотри версию/changelog,
|
2. На чистой ветке выполни `npm run release:preview`, просмотри версию/changelog,
|
||||||
затем `npm run release:prepare`. Команда создаёт `chore(release): X.Y.Z`,
|
затем `npm run release:prepare`. Команда создаёт `chore(release): X.Y.Z`,
|
||||||
обновляет manifest/lockfile/CHANGELOG; тег и push не выполняет. Не обходи
|
обновляет manifest/lockfile/CHANGELOG; тег и push не выполняет. Не обходи
|
||||||
guards ветки, чистого дерева, отсутствующих тегов или уже подготовленной версии.
|
guards ветки, чистого дерева, отсутствующих тегов или уже подготовленной версии.
|
||||||
3. Проверь diff и штатные gates этого репозитория. Отправь только release-ветку,
|
3. Проверь diff и штатные gates этого репозитория. Отправь только release-ветку,
|
||||||
открой PR в `main` с заголовком `chore(release): X.Y.Z`. После зелёных
|
открой PR в **`develop`** с заголовком `chore(release): X.Y.Z`. После зелёных
|
||||||
`verify`, `security`, `commits` выполни fast-forward-only merge. Если `main`
|
`verify`, `security`, `commits` выполни fast-forward-only merge. Если
|
||||||
изменился, согласуй историю и повтори проверки кандидата.
|
`develop` изменился, обнови release-ветку и повтори проверки кандидата.
|
||||||
4. Получи удалённый `main`, сравни его HEAD с проверенным release SHA и версией
|
4. Открой PR `develop → main` с тем же заголовком и слей fast-forward-only.
|
||||||
|
5. Получи удалённый `main`, сравни его HEAD с проверенным release SHA и версией
|
||||||
manifest/lockfile. Создай аннотированный `vX.Y.Z` именно на этом SHA:
|
manifest/lockfile. Создай аннотированный `vX.Y.Z` именно на этом SHA:
|
||||||
`git tag -a vX.Y.Z <release-sha> -m "Release X.Y.Z"`, затем отправь только
|
`git tag -a vX.Y.Z <release-sha> -m "Release X.Y.Z"`, затем отправь только
|
||||||
`git push origin refs/tags/vX.Y.Z`. Не запускай release-it повторно на `main`.
|
`git push origin refs/tags/vX.Y.Z`. Не запускай release-it повторно на `main`.
|
||||||
5. Верни release-коммит через PR `main → develop` без squash/rebase; проверь,
|
|
||||||
что `develop` содержит тег. Опубликованные теги не перемещай. При сбое после
|
Обратная синхронизация не нужна: release-коммит попал в `develop` на шаге 3.
|
||||||
merge создай отсутствующий тег без нового bump; до merge исправь и проверь
|
Опубликованные теги не перемещай. При сбое после merge создай отсутствующий тег
|
||||||
существующий кандидат без повторного повышения версии.
|
без нового bump; до merge исправь и проверь существующий кандидат без
|
||||||
|
повторного повышения версии.
|
||||||
|
|
||||||
|
Freeze релиза не поддерживается: пока открыт релизный PR, `develop` не двигают.
|
||||||
|
Срочный fix — обычная ветка `fix/*` от `develop` с последующим релизом;
|
||||||
|
пути от `main` нет, он сделал бы `main` не-предком `develop`. Исключение
|
||||||
|
требует явного решения: `release/hotfix-<name>` от `main`, PR в `main`, тег,
|
||||||
|
затем merge `main → develop` для восстановления инварианта.
|
||||||
|
|
||||||
`feat:` повышает minor, `fix:`/`perf:` и прочие разрешённые типы — patch;
|
`feat:` повышает minor, `fix:`/`perf:` и прочие разрешённые типы — patch;
|
||||||
`!`/`BREAKING CHANGE:` — major, включая версии до 1.0. `chore(release):`
|
`!`/`BREAKING CHANGE:` — major, включая версии до 1.0. `chore(release):`
|
||||||
исключён из рекомендации и changelog. Конфигурация — `.release-it.json`.
|
исключён из рекомендации и changelog. Конфигурация — `.release-it.json`.
|
||||||
В Gitea для `main`/`develop` должны быть запрещены прямой/force push и обход
|
В Gitea для `main`/`develop` должны быть запрещены прямой/force push и обход
|
||||||
администратором, обязательны `verify`, `security`, `commits`; теги `v*` защищены.
|
администратором, включена блокировка устаревшей ветки, обязательны `verify`,
|
||||||
|
`security`, `commits`; теги `v*` защищены. Линейность обеспечивают merge styles
|
||||||
|
репозитория: merge-коммиты и rebase `--no-ff` выключены, доступны squash,
|
||||||
|
rebase-merge и fast-forward-only; стиль по умолчанию — squash.
|
||||||
Workflow сам не включает эти настройки. Отдельное review approval для одного
|
Workflow сам не включает эти настройки. Отдельное review approval для одного
|
||||||
владельца не требуется. Npm publishing, deploy и Gitea Releases не включены.
|
владельца не требуется. Npm publishing, deploy и Gitea Releases не включены.
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
**Status:** Accepted; supersedes the refresh-token reuse rule in ADR-006.
|
**Status:** Accepted; supersedes the refresh-token reuse rule in ADR-006.
|
||||||
|
|
||||||
|
Cookie-clearing policy уточнена в [ADR 018](018-minimal-runtime-coordination.md):
|
||||||
|
неуспешный refresh больше не удаляет cookie. CAS и отказ superseded token сохранены.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Два одновременных запроса могут предъявить один действительный refresh token. Отзыв Session для проигравшего запроса делает недействительной сессию успешного запроса, а сохранение отдельной истории token или grace-window для различения replay добавляет непропорциональное состояние локальному MVP.
|
Два одновременных запроса могут предъявить один действительный refresh token. Отзыв Session для проигравшего запроса делает недействительной сессию успешного запроса, а сохранение отдельной истории token или grace-window для различения replay добавляет непропорциональное состояние локальному MVP.
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# 016. Версионируемый комплект развёртывания
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
|
||||||
|
Frontend и backend выпускаются независимо. Выбор их версий непосредственно
|
||||||
|
на сервере перекладывает подбор совместимой пары на оператора при каждом
|
||||||
|
обновлении. Принято решение выпускать комплект в репозитории `deploy`:
|
||||||
|
его релиз закрепляет проверенную пару конкретных версий обоих образов.
|
||||||
|
Независимые релизы компонентов сохраняются; выпуск компонента сам по себе
|
||||||
|
не означает готовность нового релиза развёртывания.
|
||||||
|
|
||||||
|
Оператор подключается к серверу по SSH и одной командой запускает обновление
|
||||||
|
до выбранного релиза развёртывания. При обновлении backend команда должна
|
||||||
|
скачать образы до его остановки, сохранить резервную копию остановленной базы,
|
||||||
|
выполнить миграции и проверить запуск. Ограничения
|
||||||
|
[ADR 015](015-single-server-sqlite-deployment.md)
|
||||||
|
сохраняются: один backend, краткий простой, неуспешная миграция блокирует запуск;
|
||||||
|
откат образа не восстанавливает базу автоматически.
|
||||||
|
|
||||||
|
## Доставка и подготовка сервера
|
||||||
|
|
||||||
|
На сервере хранится Git checkout `deploy`. Команда обновления получает
|
||||||
|
выбранный тег комплекта и загружает закреплённые образы из Nexus;
|
||||||
|
сборка приложения на сервере не требуется.
|
||||||
|
|
||||||
|
Docker с Compose, внешний HTTPS и доступ к registry считаются подготовленными.
|
||||||
|
README комплекта должен описывать последовательность этой разовой подготовки
|
||||||
|
и подключения существующей установки. Автоматическое provisioning сервера
|
||||||
|
в комплект не входит.
|
||||||
|
|
||||||
|
Рабочие данные, сертификат доверия T-Bank и секреты остаются вне обновляемого
|
||||||
|
checkout. При подключении существующей установки сохраняются её БД,
|
||||||
|
JWT secret и ключ шифрования. Сертификат `certs/tbank-ca.pem` монтируется
|
||||||
|
в контейнер только для чтения; путь внутри контейнера задаётся через
|
||||||
|
`TBANK_CA_CERT_PATH`. Этот сертификат не обслуживает входящий HTTPS.
|
||||||
|
|
||||||
|
## Резервные копии
|
||||||
|
|
||||||
|
Перед обновлением backend создаётся согласованная копия всего каталога данных
|
||||||
|
при остановленном backend. Копии хранятся в локальном `backups/` вне checkout,
|
||||||
|
без автоматического удаления; очистку выполняет оператор вручную.
|
||||||
|
Такие копии позволяют восстановиться после неудачного обновления, но сами
|
||||||
|
по себе не защищают от потери диска сервера.
|
||||||
|
|
||||||
|
## Обновление и сбои
|
||||||
|
|
||||||
|
Если релиз меняет только frontend, обновляется только его контейнер:
|
||||||
|
backend не останавливается, миграции и резервное копирование БД для этого
|
||||||
|
обновления не выполняются. Этот путь применим только при неизменных образе
|
||||||
|
и конфигурации backend.
|
||||||
|
|
||||||
|
Если миграции прошли, но новый backend не прошёл проверку запуска, обновление
|
||||||
|
завершается с ошибкой, неисправный backend останавливается. Оператор получает
|
||||||
|
диагностику, сведения о предыдущей версии и путь к резервной копии.
|
||||||
|
Автоматического отката нет: прежний образ может быть несовместим с новой
|
||||||
|
схемой. Откат образа и восстановление базы остаются отдельными явными действиями
|
||||||
|
по правилам ADR 015.
|
||||||
|
|
||||||
|
## Выпуск комплекта
|
||||||
|
|
||||||
|
После публикации образов компонентов отдельно готовится релиз `deploy`:
|
||||||
|
фиксируется совместимая пара версий, выполняется её проверка и выпускается
|
||||||
|
тег комплекта. Публикация компонента не создаёт релиз комплекта автоматически
|
||||||
|
и не запускает обновление сервера. Оператор выбирает выпущенный тег при
|
||||||
|
запуске команды развёртывания.
|
||||||
|
|
||||||
|
## Состояние реализации
|
||||||
|
|
||||||
|
Комплект и команда реализованы в отдельном репозитории `deploy`.
|
||||||
|
Публикация первого тега и обновление рабочего сервера — отдельные действия.
|
||||||
|
[Контейнерная поставка](../architecture/deployment.md) описывает границы,
|
||||||
|
пошаговая инструкция находится в README комплекта.
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# 017. Локальное чтение данных T-Bank и фоновая синхронизация
|
||||||
|
|
||||||
|
**Status:** Accepted — историческое архитектурное направление. Решения ниже о
|
||||||
|
request-driven запуске, ручном обновлении и отсутствии периодического обхода
|
||||||
|
заменены [ADR 019](019-hourly-reads-and-online-instrument-reference.md).
|
||||||
|
|
||||||
|
Prefetch policy ниже заменена [ADR 018](018-minimal-runtime-coordination.md):
|
||||||
|
свечи загружаются по запрошенному диапазону, без предварительной загрузки
|
||||||
|
всей истории бумаг портфеля. Остальные гарантии локального хранения сохранены.
|
||||||
|
|
||||||
|
Реализация `persist-tbank-data` добавлена в рабочие ветки приложения;
|
||||||
|
performance/browser и canonical application gates пройдены. Это не отметка
|
||||||
|
развёртывания в production. Ниже сохранён контекст
|
||||||
|
принятия решения, а текущее устройство описывает
|
||||||
|
[модель свежести](../architecture/cross-cutting/data-freshness.md).
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Инвестиционная история не сохраняется в БД. Серверные кэши находятся в памяти
|
||||||
|
процесса, а загрузка операций, аналитики дохода и рыночной истории зависит
|
||||||
|
от повторных обращений к T-Bank. Для журнала операций запрос доступных счетов
|
||||||
|
выполняется даже перед чтением кэшированной страницы. Сохранение только
|
||||||
|
ответов без изменения этого пути не устранит ожидание внешнего API.
|
||||||
|
|
||||||
|
Нужно ускорить все экраны, использующие T-Bank, и уменьшить повторную загрузку
|
||||||
|
данных. Текущие механизмы описаны в [политике свежести](../architecture/cross-cutting/data-freshness.md)
|
||||||
|
и [журнале операций](../domains/operations/overview.md).
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Для уже загруженных данных обычный запрос frontend обслуживается из постоянного
|
||||||
|
локального хранилища без ожидания ответа T-Bank. Обновление выполняется в фоне.
|
||||||
|
Пользователь видит время последнего успешного обновления и может запросить
|
||||||
|
обновление вручную. Сохранённый портфель может ещё не содержать новую сделку;
|
||||||
|
эта задержка принята ради быстрого открытия экрана.
|
||||||
|
|
||||||
|
Синхронизация запускается только запросами пользовательских экранов и ручным
|
||||||
|
действием «Обновить». Периодических запусков, ежедневного обновления неактивных
|
||||||
|
пользователей и плановой сверки истории нет. Фоновое выполнение означает,
|
||||||
|
что уже запрошенная работа не задерживает выдачу сохранённых данных;
|
||||||
|
самостоятельное расписание обновлений оно не подразумевает.
|
||||||
|
|
||||||
|
Обычный запрос запускает обновление, только если нужных данных нет либо
|
||||||
|
истёк согласованный срок их свежести:
|
||||||
|
|
||||||
|
| Данные | Срок свежести |
|
||||||
|
| --- | --- |
|
||||||
|
| Портфель и текущие цены | 1 минута |
|
||||||
|
| Операции текущего месяца | 5 минут |
|
||||||
|
| Список доступных счетов | 5 минут |
|
||||||
|
| Справочник инструментов и календари купонов и дивидендов | 1 сутки |
|
||||||
|
| Незавершённая дневная свеча | 1 минута |
|
||||||
|
|
||||||
|
Эти сроки проверяются при запросе и не задают расписание фоновых запусков.
|
||||||
|
Аналитика дохода зависит от полноты и свежести исходных операций.
|
||||||
|
|
||||||
|
Полностью загруженные предыдущие месяцы операций повторно читаются из T-Bank
|
||||||
|
только по кнопке «Обновить» для показанного периода. Обычный запрос догружает
|
||||||
|
отсутствующие интервалы и при необходимости обновляет текущий месяц.
|
||||||
|
Завершённые дневные свечи сохраняются; ручное обновление перечитывает
|
||||||
|
показанный диапазон. Автоматическое обнаружение поздних операций и исправлений
|
||||||
|
в уже полностью загруженных прошлых месяцах не обещается.
|
||||||
|
|
||||||
|
Кнопка «Обновить» обходит проверку давности и обновляет данные текущего
|
||||||
|
экрана: выбранного счёта, доступных счетов в режиме «Все счета» либо
|
||||||
|
инструмента и показанной личной истории на его странице. Нажатие не означает
|
||||||
|
повторную загрузку всей накопленной истории всех счетов и инструментов.
|
||||||
|
|
||||||
|
Решение охватывает все экраны с данными T-Bank. Исполненные операции и рыночная
|
||||||
|
история сохраняются; текущие позиции, остатки и цены представлены последним
|
||||||
|
успешно полученным состоянием. Будущие выплаты остаются изменяемым прогнозом,
|
||||||
|
а не исполненными операциями. Это не решение о накоплении исторических снимков
|
||||||
|
стоимости портфеля.
|
||||||
|
|
||||||
|
Рыночная история бумаг текущего портфеля загружается заранее в рамках
|
||||||
|
запрошенной пользователем работы. Для остальных бумаг первая загрузка
|
||||||
|
начинается при открытии инструмента, результат сохраняется для последующего
|
||||||
|
чтения. Предварительная загрузка истории всего каталога не требуется.
|
||||||
|
|
||||||
|
При первом подключении текущий портфель и недавние операции могут отображаться
|
||||||
|
до завершения загрузки более ранней истории. Частично загруженный период явно
|
||||||
|
отличается от полного: неполные итоги нельзя выдавать за окончательные,
|
||||||
|
а отсутствие загруженных записей — за подтверждённое отсутствие операций.
|
||||||
|
Смысл полноты определён в [глоссарии](../glossary.md#полнота-истории-счёта).
|
||||||
|
|
||||||
|
Первый запрос задаёт приоритет: сначала период, нужный экрану, включая
|
||||||
|
периоды сравнения дохода, затем вся доступная история счёта. Уже запущенная
|
||||||
|
догрузка может завершиться после ухода пользователя со страницы; нового
|
||||||
|
периодического запуска это не создаёт.
|
||||||
|
|
||||||
|
Полнота относится к успешно загруженным временным интервалам. Смена месяца
|
||||||
|
сама по себе не делает завершённым последний неполный интервал прошлого
|
||||||
|
месяца: его оставшаяся часть должна быть догружена. Ошибка промежуточной
|
||||||
|
страницы не подтверждает полноту и не удаляет ранее сохранённые данные.
|
||||||
|
Незавершённая свеча также не становится завершённой только из-за смены дня:
|
||||||
|
её окончательное состояние должен подтвердить источник.
|
||||||
|
|
||||||
|
Целевой ориентир — до 500 мс для обычного серверного запроса к заполненной БД.
|
||||||
|
Это согласованная цель измерения, а не подтверждённая характеристика системы.
|
||||||
|
Объём данных и набор запросов для проверки предстоит определить.
|
||||||
|
|
||||||
|
Для этого изменения принято допущение: ID брокерской операции не меняется
|
||||||
|
со временем. Обнаружение сменившихся ID и регулярная сверка старой истории
|
||||||
|
не входят в объём работы. Это допущение проекта, а не гарантия T-Bank:
|
||||||
|
используемый protobuf-контракт допускает изменение ID. Допущение относится
|
||||||
|
к идентичности операции; оно не требует игнорировать новые значения полей,
|
||||||
|
если операция с прежним ID снова получена от брокера.
|
||||||
|
|
||||||
|
Подключение T-Bank считается постоянным в рассматриваемом сценарии.
|
||||||
|
Архив без подключения, специальная политика хранения после отключения
|
||||||
|
и работа с закрытыми счетами пока не проектируются. Это ограничение объёма
|
||||||
|
не отменяет существующие проверки доступа и обработку ошибок интеграции.
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
Ожидание свежего ответа T-Bank при каждом открытии экрана сохраняет зависимость
|
||||||
|
скорости и доступности интерфейса от брокера. Увеличение времени жизни кэша
|
||||||
|
в памяти уменьшает часть повторных запросов, но не сохраняет историю после
|
||||||
|
перезапуска и не обеспечивает локальное чтение всех нужных данных.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Интерфейсу нужны различимые состояния первичной загрузки, полноты истории,
|
||||||
|
свежести и ошибки фонового обновления. Успешная загрузка недавних операций
|
||||||
|
не подтверждает полноту более ранних периодов или периодов сравнения дохода.
|
||||||
|
|
||||||
|
Проверки локальной сессии и принадлежности личных данных сохраняются.
|
||||||
|
Способ подтверждения брокерского доступа должен позволять обслужить уже
|
||||||
|
загруженные данные без синхронного ожидания T-Bank.
|
||||||
|
|
||||||
|
Повторные запросы одного набора данных должны использовать уже выполняющуюся
|
||||||
|
загрузку. Завершённые интервалы и прогресс сохраняются в БД; после перезапуска
|
||||||
|
незавершённый интервал повторяется при следующем запросе или ручном обновлении.
|
||||||
|
Курсор брокера и максимальная дата полученной операции не доказывают полноту.
|
||||||
|
|
||||||
|
Ручное обновление периода согласует состав операций и их поля с полностью
|
||||||
|
полученным ответом источника. Только успешная загрузка всех страниц позволяет
|
||||||
|
зафиксировать полное покрытие, успешное обновление и удалить отсутствующие
|
||||||
|
в этом ответе записи периода. Ошибка не превращает частичный ответ в полный.
|
||||||
|
|
||||||
|
Личные операции и позиции сохраняют принадлежность пользователю и счёту.
|
||||||
|
Общие данные инструмента не должны содержать персональные позиции и расчёты;
|
||||||
|
поздний результат прежнего подключения не публикуется для нового подключения.
|
||||||
|
|
||||||
|
Существующие HTTP-контракты пока не поддерживают все необходимые состояния
|
||||||
|
загрузки и полноты; их изменение входит в будущую реализацию.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# 018. Минимальная runtime-координация
|
||||||
|
|
||||||
|
**Status:** Accepted. Уточняет cookie policy ADR 009 и prefetch policy ADR 017.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Изоляция владельцев данных, атомарные финансовые изменения и локальное чтение
|
||||||
|
не требуют отдельного протокола согласования каждого HTTP-ответа. Дополнительная
|
||||||
|
координация оправдана только наблюдаемым поведением, которое иначе нарушается.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
- Refresh сохраняет атомарную ротацию. Неуспешный refresh не меняет cookie:
|
||||||
|
поздний отказ не должен удалять cookie успешного запроса. Logout очищает cookie.
|
||||||
|
- Frontend объединяет refresh и упорядочивает cookie-writing requests только
|
||||||
|
внутри вкладки. Cross-tab auth synchronization не является требованием;
|
||||||
|
после изменения сессии в другой вкладке допускаются reload или re-login.
|
||||||
|
- Отменой, сбросом и публикацией query results владеет TanStack Query.
|
||||||
|
Смена session/connection очищает соответствующий cache; отдельные registries
|
||||||
|
HTTP-запросов, access epochs и URL-wide latest-read counters не используются.
|
||||||
|
Локальная auth boundary предотвращает применение старого auth result и
|
||||||
|
повтор mutation под другой identity.
|
||||||
|
- Status GET читает только состояние работы. Открытый экран перечитывает
|
||||||
|
resources через `sync=observe` во время работы и после завершения.
|
||||||
|
Сверка hashes между status и data GET, публичные progress counters и
|
||||||
|
невыдаваемые sync states не нужны. Полнота истории остаётся в resource metadata.
|
||||||
|
- Свечи загружаются для запрошенного диапазона. Предзагрузки всей истории
|
||||||
|
портфеля и автоматического расширения короткого периода до `all` нет.
|
||||||
|
- Backend формирует согласованные entries и aggregates из одних исходных
|
||||||
|
данных; frontend доверяет этому contract и не выполняет вторую сверку групп.
|
||||||
|
- Request context и HTTP completion принадлежат `nestjs-pino`/`pino-http`.
|
||||||
|
Проект сохраняет safe field filtering, redaction и route-template logging.
|
||||||
|
- Deployment всегда выполняет один stop → backup → migration → readiness
|
||||||
|
цикл. No-op и frontend-only оптимизации не обещаются; короткий простой принят.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Первое открытие нового диапазона свечей может ждать загрузку. Наблюдение активной
|
||||||
|
работы может перечитать неизменившиеся локальные данные, зато не требует
|
||||||
|
checksum protocol. Повтор deployment создаёт новый backup и может остановить API.
|
||||||
|
|
||||||
|
DB ownership подключения, conditional publication, coverage, staging,
|
||||||
|
банковские intent fingerprints и версии записей сохраняются. Отмена HTTP
|
||||||
|
не считается отменой уже выполняющейся server-side transaction.
|
||||||
|
Сохранённый полный forecast не заменяется candidate с изменившимися dependencies.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# 019. Часовой обход и онлайн-справочник инструментов
|
||||||
|
|
||||||
|
**Status:** Accepted. Заменяет request-driven, manual-refresh и
|
||||||
|
no-periodic-walk части [ADR 017](017-local-tbank-reads-and-background-sync.md)
|
||||||
|
и дополняет [ADR 018](018-minimal-runtime-coordination.md). Правило «справочные
|
||||||
|
сведения запрашиваются онлайн» дополнено исключением для сектора и бренда
|
||||||
|
актива позиции — [ADR
|
||||||
|
022](022-stored-instrument-classification.md).
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
- Личные T-Bank данные текущего подключения обновляются владельцами источников
|
||||||
|
через durable hourly walk; его результат ограничен этой connection boundary.
|
||||||
|
Обычный GET и retry читают только сохранённый результат и не запускают
|
||||||
|
personal work.
|
||||||
|
- Только первое открытие неподтверждённого исторического интервала operations
|
||||||
|
и первого неоткрытого диапазона candles может получить соответственно этот
|
||||||
|
интервал или диапазон. Это не manual sync; повторное чтение не расширяет и не
|
||||||
|
запускает такую работу заново.
|
||||||
|
- Portfolio, operations, forecasts и события сохраняют собственные complete
|
||||||
|
результаты и время последнего успешного чтения. Результат ресурса
|
||||||
|
(freshness, completeness, `lastSuccessfulAt`, availability) независим от
|
||||||
|
resource-relevant последней часовой попытки: поздняя partial/failed попытка
|
||||||
|
не заменяет сохранённый complete result. При его отсутствии `data: null`.
|
||||||
|
- Detail, company, catalog search и ticker lookup получают ответ онлайн через
|
||||||
|
существующие integration clients. Их ошибки не заменяют сохранённые
|
||||||
|
финансовые данные или подписи snapshot.
|
||||||
|
- Старые справочные строки не являются источником актуальности и удаляются
|
||||||
|
только отдельной совместимой миграцией после проверки FK и свечей.
|
||||||
|
- Клиент повторяет открытые инвестиционные GET не чаще одного раза в 30 минут;
|
||||||
|
retry повторяет чтение и не создаёт screen sync work. Наблюдение последней
|
||||||
|
попытки, включая Accounts `sync`, не является командой.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Карточки инструментов зависят от доступности публичного upstream в момент
|
||||||
|
чтения. Финансовые экраны сохраняют последний успешный snapshot, а статус
|
||||||
|
последнего hourly walk показывается отдельно. OpenAPI backend остаётся
|
||||||
|
единственным исполняемым источником HTTP-контракта; frontend использует
|
||||||
|
опубликованный snapshot.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# 020. Evidence for hourly sync read boundaries
|
||||||
|
|
||||||
|
**Status:** Accepted. Evidence for [ADR 019](019-hourly-reads-and-online-instrument-reference.md).
|
||||||
|
|
||||||
|
## Measurements
|
||||||
|
|
||||||
|
The backend persisted-read budget was measured with `npm run test:performance`
|
||||||
|
using a synthetic SQLite dataset: three accounts, 100,000 operations per
|
||||||
|
account, 200 instruments, and 5,000 candles per instrument. The run used Node
|
||||||
|
24.21.0 on Apple M1 Pro with 32 GiB RAM and a failing synthetic upstream after
|
||||||
|
population. It measured 100 reads per route family, excluding transport
|
||||||
|
limiter cost.
|
||||||
|
|
||||||
|
The generated evidence is written by the test to its temporary
|
||||||
|
`persistent-budget.json` file. This is a repeatable measurement, not a
|
||||||
|
production SLA: it does not include network latency, real T-Bank quotas, or
|
||||||
|
multi-process SQLite contention.
|
||||||
|
|
||||||
|
The successful run produced a 617,263,104-byte SQLite database and measured
|
||||||
|
zero upstream calls during the persisted-read phase. Backend p50/p95 response
|
||||||
|
times were approximately 1.3/2.3 ms for account and portfolio reads,
|
||||||
|
3.9/4.5 ms for account operations, 6.7/7.2 ms for aggregate operations,
|
||||||
|
40.0/41.5 ms for account income analytics, and 128.1/134.2 ms for aggregate
|
||||||
|
income analytics. These figures are local synthetic measurements only.
|
||||||
|
|
||||||
|
## Durability
|
||||||
|
|
||||||
|
The migration durability check passes against a separately built repository at
|
||||||
|
the pre-history migration prefix. It verifies preservation of users, sessions,
|
||||||
|
T-Bank connection data, bank-card data, operations, categories, archive state,
|
||||||
|
and replay behavior across migration and restart.
|
||||||
|
|
||||||
|
The executed command was `node test/bank-cards-durability.cjs` with a separately
|
||||||
|
built pre-history repository. It completed with `PASS durability`.
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
|
||||||
|
The configured T-Bank start limiter is `TBANK_RATE_LIMIT_PER_SECOND`, default
|
||||||
|
`5` per backend instance. The hourly scheduler therefore remains a single
|
||||||
|
durable reservation owner; the measurement does not justify increasing this
|
||||||
|
limit or changing the hourly cadence. Any production tuning requires current
|
||||||
|
T-Bank documentation and a new measurement with the deployed topology.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 021. Тихое чтение и редкие подтверждения
|
||||||
|
|
||||||
|
**Status:** Accepted. Заменяет manual-refresh часть
|
||||||
|
[ADR 019](019-hourly-reads-and-online-instrument-reference.md) на стороне
|
||||||
|
интерфейса и уточняет клиентскую часть
|
||||||
|
[ADR 007](007-react-query-fetch-policy.md). Не меняет владельцев обновления
|
||||||
|
и границы чтения из ADR 019.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Экраны просмотра перекладывали на пользователя контроль над получением данных:
|
||||||
|
кнопки «Обновить» и «Повторить», индикатор фонового обновления и сообщение
|
||||||
|
о неудачной синхронизации повторялись в разных разделах. Пользователь приходит
|
||||||
|
смотреть финансы, а не обслуживать загрузку; восстановление после временного
|
||||||
|
сбоя не должно зависеть от нажатия кнопки.
|
||||||
|
|
||||||
|
При этом состояние самих данных остаётся значимым: время последнего успешного
|
||||||
|
результата источника, устаревание и исход часовой попытки влияют на доверие
|
||||||
|
к показанным суммам и не могут быть просто скрыты.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
- Автоматическое чтение клиента не сообщает о себе. Экраны просмотра не
|
||||||
|
показывают кнопок ручного обновления и повтора, индикаторов фонового
|
||||||
|
перечитывания и технических предупреждений о собственном запросе.
|
||||||
|
- Временные сбои GET восстанавливаются сами: ограниченный срок ожидания запроса
|
||||||
|
и не более двух повторов с возрастающими паузами, затем возврат к штатной
|
||||||
|
работе при появлении результата. Пользователь не инициирует восстановление.
|
||||||
|
- Состояние источника из `meta` остаётся видимым. Время последнего успешного
|
||||||
|
результата, признак устаревания, исход последней часовой попытки и неполнота
|
||||||
|
показываются текстом либо доступно именованным элементом, а не только цветом.
|
||||||
|
Это свойство данных, а не отчёт о техническом процессе, и оно не превращается
|
||||||
|
в действие пользователя.
|
||||||
|
- Доступный результат сохраняется при обновлении и временной ошибке. Неполнота
|
||||||
|
и подтверждённая пустота остаются различимыми: неизвестная сумма не становится
|
||||||
|
нулём, незавершённый расчёт не выдаётся за окончательный, незагруженный список
|
||||||
|
не выдаётся за отсутствие записей.
|
||||||
|
- Первичное ожидание ограничено по времени и показывает структуру будущего
|
||||||
|
содержимого. По его истечении экран переходит в спокойную локальную
|
||||||
|
недоступность вместо бесконечной загрузки и сам заменяет её содержимым после
|
||||||
|
восстановления. Сбой одного блока не скрывает остальные доступные блоки.
|
||||||
|
- Ошибки явных действий пользователя сохраняются и остаются видимыми. Решение
|
||||||
|
касается только чтения; отправка формы, изменение данных и сброс фильтра
|
||||||
|
по-прежнему сообщают результат, в том числе неуспешный.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Пользователь не может ускорить обновление вручную: нормальным владельцем
|
||||||
|
обновления остаётся часовой обход из ADR 019, и отсутствие кнопки не меняет его
|
||||||
|
срок. Если источник недоступен долго, экран показывает прежние данные с
|
||||||
|
обозначением устаревания, а не приглашение повторить.
|
||||||
|
|
||||||
|
Автоматический повтор ограничен: он покрывает короткие сетевые сбои, но не
|
||||||
|
длительную недоступность. Ошибки доступа, домена и отмены не повторяются,
|
||||||
|
mutations не повторяются вовсе.
|
||||||
|
|
||||||
|
Состояние источника и состояние клиентского чтения описывают разные вещи и
|
||||||
|
не подменяют друг друга. `Portfolio.isStale` сохраняет смысл возраста снимка;
|
||||||
|
ошибка клиентского перечитывания его не меняет.
|
||||||
|
|
||||||
|
Детали поведения — в
|
||||||
|
[ожидании и восстановлении интерфейса](../architecture/cross-cutting/ui-resilience.md)
|
||||||
|
и [свежести данных](../architecture/cross-cutting/data-freshness.md).
|
||||||
|
Согласованные требования принадлежат workspace-спецификациям
|
||||||
|
`frontend-user-feedback`, `frontend-data-resilience` и
|
||||||
|
`frontend-loading-presentation`.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# 022. Хранимая классификация инструментов T-Bank
|
||||||
|
|
||||||
|
**Status:** Accepted. Дополняет [ADR
|
||||||
|
019](019-hourly-reads-and-online-instrument-reference.md) исключением из
|
||||||
|
правила «справочные сведения запрашиваются онлайн». Решение утверждено
|
||||||
|
владельцем в [Issue
|
||||||
|
WealthLedger/main#48](https://git.personal.ksv741.ru/WealthLedger/main/issues/48).
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
ADR 019 установил, что справочные сведения инструментов (detail, lookup,
|
||||||
|
каталоги) запрашиваются онлайн и не сохраняются как долговременный справочник:
|
||||||
|
«старые справочные строки не являются источником актуальности». Вкладка
|
||||||
|
«Концентрация» требует для каждой позиции её сектора и эмитента — бренда
|
||||||
|
актива по данным T-Bank. Сектор есть только в описаниях отдельных типов
|
||||||
|
инструментов (`share`, `bond`, `etf`), а эмитент — в описании актива по
|
||||||
|
`asset_uid` инструмента; из названия, тикера, ISIN или `logoName` его вывести
|
||||||
|
нельзя. Запрашивать эти поля онлайн при каждом открытии экрана нельзя:
|
||||||
|
ответ Portfolio не ждёт T-Bank, а разрезы должны считаться по уже сохранённому
|
||||||
|
снимку.
|
||||||
|
|
||||||
|
Существующий metadata-кэш (`TbankInstrument`) не подходит: общий ответ
|
||||||
|
`GetInstrumentBy` не содержит `sector`, а его `brand` — данные логотипа,
|
||||||
|
а не `{uid, name}` бренда актива.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
- Сектор и бренд актива позиции хранятся как последнее известное значение в
|
||||||
|
глобальной таблице `TbankInstrumentClassification` (провайдер, ключ
|
||||||
|
инструмента, сектор и время его получения, uid актива и uid/название бренда
|
||||||
|
с временем получения). Данные публичные, без привязки к пользователю или
|
||||||
|
подключению; ограждение по connection revision к ним не применяется, публикация
|
||||||
|
Snapshot ограждается как прежде.
|
||||||
|
- Каждое поле обновляется в часовом обходе при его первой попытке, начавшейся
|
||||||
|
не ранее чем через 24 часа после последнего успешного получения этого поля;
|
||||||
|
пока значение не получено, попытка повторяется в каждом обходе. Сектор и
|
||||||
|
бренд обновляются независимо.
|
||||||
|
- Неуспешное получение не мешает публикации Portfolio Snapshot и не влияет на
|
||||||
|
его свежесть; сохранённое значение остаётся доступным. Отказ token
|
||||||
|
обрабатывается как отказ token при обходе Portfolio. Ответ «инструмент не
|
||||||
|
найден» сохраняет прежнее значение и перезапускает отсчёт 24 часов.
|
||||||
|
- Классификация не входит в `meta.dependencies` Portfolio и не меняет
|
||||||
|
`isStale` и `lastSuccessfulAt`.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Это первый долговременный справочник T-Bank в системе: ADR 019 теряет
|
||||||
|
характер «только онлайн» для этой области и дополняется, а не заменяется.
|
||||||
|
- Разрезы концентрации считаются по снимку без обращений к T-Bank; число
|
||||||
|
вызовов растёт ограниченно — не более одной попытки на поле в сутки на
|
||||||
|
позицию, при первом обходе после выката возможен разовый всплеск, сглаженный
|
||||||
|
общим лимитером и очередью.
|
||||||
|
- Значение может быть устаревшим до 24 часов; это приемлемо для группировки
|
||||||
|
позиций, но классификация не является источником актуальных данных брокера.
|
||||||
|
- Второй потребитель хранимой классификации — условие пересмотра решения
|
||||||
|
(выделение независимого процесса обновления, отдельные сроки).
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# 023. Результат инвестирования относительно индекса Мосбиржи
|
||||||
|
|
||||||
|
**Status:** Accepted. **Date:** 2026-11-07. Дополняет [ADR
|
||||||
|
019](019-hourly-reads-and-online-instrument-reference.md) исключением для
|
||||||
|
публичной истории закрытий индексов MOEX (по образцу
|
||||||
|
[ADR 022](022-stored-instrument-classification.md)). Решение оформлено в
|
||||||
|
[Issue WealthLedger/main#54](https://git.personal.ksv741.ru/WealthLedger/main/issues/54)
|
||||||
|
и утверждённом change `specs/openspec/changes/2026-11-07-investment-performance-vs-index/`
|
||||||
|
в репозитории specs.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Вкладка «Результат» должна показать, как счёт или «Все счета» справились с
|
||||||
|
деньгами, которые в него внесли, и сравнить это с вложением тех же денег в
|
||||||
|
индекс. Простая доходность «стоимость / вложено − 1» искажает оценку, когда
|
||||||
|
пополнения и выводы распределены во времени. Доходность по времени (TWR)
|
||||||
|
требует истории стоимости на каждую дату потока, а её в системе нет: хранится
|
||||||
|
только последний Portfolio.
|
||||||
|
|
||||||
|
Сравнение с ценовым IMOEX не отвечает на вопрос пользователя: IMOEX не
|
||||||
|
включает дивиденды и купоны, а эталон должен учитывать их так же, как
|
||||||
|
портфель. ADR 019 запрещает хранить справочные данные как долговременный
|
||||||
|
источник; индексы — публичные временные ряды, для которых онлайн-запрос при
|
||||||
|
каждом открытии вкладки неприемлем по задержке и лимитам внешнего сервиса.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
- **«Доходность» для «Результата инвестирования» — money-weighted return (XIRR)**
|
||||||
|
по внешним денежным потокам счёта и текущей оценке Portfolio. Термин
|
||||||
|
«доходность» снимается с запрета только для этого понятия; прочие понятия
|
||||||
|
«не доходность» сохраняют прежний текст.
|
||||||
|
- **Сравнение — теневой портфель в индексе.** Те же внешние потоки
|
||||||
|
конвертируются в единицы индекса по закрытию дня потока (или ближайшего
|
||||||
|
предыдущего торгового дня) и оцениваются по закрытию на дату расчёта.
|
||||||
|
Эталон — полный доходный индекс MCFTRR; XIRR индекса считается на тех же
|
||||||
|
потоках. Это идея PME по Long–Nickels; в интерфейсе — «те же деньги в
|
||||||
|
индексе Мосбиржи».
|
||||||
|
- **IMOEX — только справочный индекс.** Он показывается рядом, но не
|
||||||
|
участвует в «опережении» и не является базой сравнения.
|
||||||
|
- **Только RUB.** Счёт с потоками или стоимостью в другой валюте не
|
||||||
|
рассчитывается (причина `NON_RUB_ACTIVITY`); конвертация валют не
|
||||||
|
выполняется.
|
||||||
|
- **Период** начинается с первого внешнего потока, не с открытия счёта.
|
||||||
|
Короче года — «за N дн.» без annualization; `basis = annual` только если
|
||||||
|
деньги вложены от 365 дней.
|
||||||
|
- **Хранение закрытий индексов.** Пара (индекс, торговый день по Москве) →
|
||||||
|
CLOSE хранится локально с upsert. Это исключение из ADR 019: публичная
|
||||||
|
история индекса, не привязанная к пользователю или подключению, хранится
|
||||||
|
как долговременные данные. Остальные справочные сведения ADR 019 не
|
||||||
|
меняются.
|
||||||
|
|
||||||
|
### Отклонённые варианты
|
||||||
|
|
||||||
|
- **Простой ROI** (`стоимость / вложено − 1`): искажён моментами пополнений
|
||||||
|
и выводов, поэтому не сравнивается с индексом на тех же деньгах.
|
||||||
|
- **TWR (time-weighted return):** требует стоимости портфеля на каждую дату
|
||||||
|
потока; история оценок в системе отсутствует. Сохранение такой истории
|
||||||
|
вне этого решения.
|
||||||
|
- **Ценовой IMOEX как ориентир:** не учитывает дивиденды и купоны, поэтому
|
||||||
|
сравнение с портфелем, где они учтены, смещено.
|
||||||
|
- **Конвертация валют (FX):** потребовала бы исторических курсов и
|
||||||
|
отдельного решения о методике; не нужна для личного RUB-учёта.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Результат зависит от сохранённых операций T-Bank и последнего полного
|
||||||
|
Portfolio: неполная история или неполная оценка дают `data: null` или
|
||||||
|
`completeness: incomplete` по правилам change, а не подменяются оценкой.
|
||||||
|
- Первая загрузка индекса догружает историю от `from − 10 дней`; при
|
||||||
|
недостатке закрытий появляется `INDEX_HISTORY_UNAVAILABLE`. Фоновая
|
||||||
|
синхронизация индексов не вводится.
|
||||||
|
- Исключение из ADR 019 ограничено закрытиями индексов MOEX.
|
||||||
|
|
||||||
|
### Оговорки и условия источника
|
||||||
|
|
||||||
|
- **Налоги.** MCFTRR при расчёте дивидендов и купонов использует ставки для
|
||||||
|
организаций, а не НДФЛ физического лица. Индекс — ориентир альтернативы,
|
||||||
|
а не точная модель налогов пользователя. Оговорка фиксируется в методике
|
||||||
|
(«Как считали»).
|
||||||
|
- **Данные MOEX ISS.** ISS бесплатно отдаёт данные «для ознакомления».
|
||||||
|
Приложение личное и локальное, данные не распространяются; источник
|
||||||
|
указывается в «Как считали».
|
||||||
|
- **Переводы между счетами.** Совпадение суммы и времени операций ещё не
|
||||||
|
доказывает внутренний перевод. Пока нет подтверждённой связи, такие счета
|
||||||
|
исключаются из рассчитанной части «Все счета» с явной причиной неполноты.
|
||||||
|
- **Несколько решений XIRR.** Процент не показывается: нельзя выбрать
|
||||||
|
произвольную ставку. Прибыль в рублях остаётся видна.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Issue: [WealthLedger/main#54](https://git.personal.ksv741.ru/WealthLedger/main/issues/54)
|
||||||
|
- Change: `specs/openspec/changes/2026-11-07-investment-performance-vs-index/`
|
||||||
|
(proposal, system-analysis «Расчёт» и «Индексы MOEX», design, Decisions backend 3, 5)
|
||||||
@@ -20,3 +20,11 @@
|
|||||||
- [013 — НКР как первый источник кредитных рейтингов облигаций](013-nkr-bond-credit-ratings.md)
|
- [013 — НКР как первый источник кредитных рейтингов облигаций](013-nkr-bond-credit-ratings.md)
|
||||||
- [014 — Актуальная OpenAPI-схема как вход frontend](014-trusted-openapi-input.md)
|
- [014 — Актуальная OpenAPI-схема как вход frontend](014-trusted-openapi-input.md)
|
||||||
- [015 — SQLite в ограниченном single-server deployment](015-single-server-sqlite-deployment.md)
|
- [015 — SQLite в ограниченном single-server deployment](015-single-server-sqlite-deployment.md)
|
||||||
|
- [016 — Версионируемый комплект развёртывания](016-versioned-deployment-bundle.md)
|
||||||
|
- [017 — Локальное чтение данных T-Bank и фоновая синхронизация](017-local-tbank-reads-and-background-sync.md)
|
||||||
|
- [018 — Минимальная runtime-координация](018-minimal-runtime-coordination.md)
|
||||||
|
- [019 — Часовой обход и онлайн-справочник инструментов](019-hourly-reads-and-online-instrument-reference.md)
|
||||||
|
- [020 — Evidence for hourly sync read boundaries](020-hourly-sync-verification-evidence.md)
|
||||||
|
- [021 — Тихое чтение и редкие подтверждения](021-quiet-reading-and-rare-confirmations.md)
|
||||||
|
- [022 — Хранимая классификация инструментов T-Bank](022-stored-instrument-classification.md)
|
||||||
|
- [023 — Результат инвестирования относительно индекса Мосбиржи](023-investment-performance-vs-index.md)
|
||||||
|
|||||||
+44
-26
@@ -76,8 +76,8 @@ prerequisites: локальный smoke не подтверждает registry/s
|
|||||||
| Часть | Ответственность |
|
| Часть | Ответственность |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| React SPA | Экраны, навигация, формы, состояние входа и клиентский кэш ответов; форматирование денежных значений |
|
| React SPA | Экраны, навигация, формы, состояние входа и клиентский кэш ответов; форматирование денежных значений |
|
||||||
| Backend API | Локальная идентичность, сессии, проверка доступа, подключение брокера, нормализация внешних данных, точные расчёты и серверные кэши |
|
| Backend API | Локальная идентичность, сессии, проверка доступа, подключение брокера, нормализация внешних данных, точные расчёты и синхронизация сохранённых источников |
|
||||||
| SQLite | Долговременное хранение локальных пользователей, сессий, зашифрованных подключений и ручной банковской истории |
|
| SQLite | Долговременное хранение локальных пользователей, сессий, зашифрованных подключений, ручной банковской истории и инвестиционных источников с coverage/work |
|
||||||
| Внешние системы | Данные брокера, публичные сведения об инструментах и изображения в пределах своей ответственности |
|
| Внешние системы | Данные брокера, публичные сведения об инструментах и изображения в пределах своей ответственности |
|
||||||
|
|
||||||
Frontend организован слоями `app → pages → features → entities → shared`:
|
Frontend организован слоями `app → pages → features → entities → shared`:
|
||||||
@@ -86,6 +86,20 @@ Frontend организован слоями `app → pages → features → ent
|
|||||||
где локальная аутентификация отделена от подключения брокера и инвестиционных
|
где локальная аутентификация отделена от подключения брокера и инвестиционных
|
||||||
сценариев. Это внутренняя организация приложений, не набор микросервисов.
|
сценариев. Это внутренняя организация приложений, не набор микросервисов.
|
||||||
|
|
||||||
|
Маршруты и AppShell принадлежат `app`. Page controller/VM управляет запросами,
|
||||||
|
формой и восстановлением; presentation получает данные и callbacks, поля
|
||||||
|
могут использовать контекст React Hook Form. В OperationForm controller
|
||||||
|
сохраняет intent операции, черновик и явное принятие актуальной версии после
|
||||||
|
конфликта. В account-income чистые преобразования периода принадлежат модели
|
||||||
|
страницы и не связывают форму с её запросами.
|
||||||
|
|
||||||
|
Локальные packages tokens/ui/icons образуют [визуальную основу](../design/overview.md#общая-основа-и-каталог)
|
||||||
|
внутри frontend и входят в сборку SPA. Это библиотеки, а не дополнительные
|
||||||
|
runtime-контейнеры. Провайдеры темы принадлежат приложению и Storybook;
|
||||||
|
предметные компоненты и API-клиент остаются в приложении. Отдельный API-пакет
|
||||||
|
и registry-поставка визуальных пакетов не реализованы. Существующие Docker
|
||||||
|
images приложения не означают независимого npm-выпуска этих библиотек.
|
||||||
|
|
||||||
Финансовые HTTP-запросы браузера идут через backend. Пользователь вводит token
|
Финансовые HTTP-запросы браузера идут через backend. Пользователь вводит token
|
||||||
T-Bank в форме и отправляет его для сохранения, но API не возвращает token
|
T-Bank в форме и отправляет его для сохранения, но API не возвращает token
|
||||||
в ответах; SPA не использует его для прямых брокерских запросов. Прямая
|
в ответах; SPA не использует его для прямых брокерских запросов. Прямая
|
||||||
@@ -120,28 +134,32 @@ credentials не являются сохраняемым представлен
|
|||||||
или перезапуска. Архив обратим и сохраняет историю. Подробности —
|
или перезапуска. Архив обратим и сохраняет историю. Подробности —
|
||||||
[банковские карты](../domains/cards/overview.md).
|
[банковские карты](../domains/cards/overview.md).
|
||||||
|
|
||||||
Сессии и подключение связаны с владельцем. Access token подтверждает
|
Сессии и подключение связаны с владельцем. Backend локально проверяет
|
||||||
локального пользователя, но `accountId` из URL сам по себе не даёт права
|
membership текущей revision перед каждым финансовым чтением. Unknown membership
|
||||||
читать брокерский счёт. Backend получает текущее подключение пользователя
|
не открывает доступ к произвольному счёту; известный отказ токена блокирует
|
||||||
и его generation перед использованием финансового кэша. Проверка доступности
|
личные данные. Ротация refresh token остаётся атомарной.
|
||||||
счёта у брокера зависит от сценария: журнал выполняет её до чтения страницы
|
|
||||||
кэша; свежий портфель одного счёта может быть возвращён без нового запроса
|
|
||||||
списка счетов. При обновлении портфеля доступ проверяется у брокера.
|
|
||||||
|
|
||||||
Ротация сессии атомарно заменяет hash текущего refresh token: два
|
SQLite хранит полный последний portfolio, операции и их подтверждённые
|
||||||
конкурентных запроса не могут оба успешно выполнить одну ротацию.
|
диапазоны, private прогнозы, сохранённые точки истории оценки, общие
|
||||||
Вызов брокера и запись в локальную базу не образуют общую транзакцию:
|
instrument/market sources, классификацию инструментов (сектор и бренд актива
|
||||||
при сохранении подключения сначала проверяется token, затем записывается
|
как последнее известное значение с временем каждого получения,
|
||||||
шифротекст. Подробности находятся в
|
`TbankInstrumentClassification`) и intent/staging.
|
||||||
[аутентификации](../domains/authentication/overview.md) и
|
Нормализованные финансовые значения сохраняются decimal-строками. Полнота
|
||||||
[интеграции T-Bank](../integrations/tbank.md).
|
отделена от свежести и пагинации. Ответ не ждёт внешних RPC; обновлением
|
||||||
|
личных данных владеет часовой обход в том же backend process через
|
||||||
|
существующий лимитер, недостающие общие источники он же ставит в фоновую
|
||||||
|
очередь. На startup обход начинает следующую попытку по обычному часовому
|
||||||
|
сроку; после нового подключения первая попытка начинается сразу.
|
||||||
|
Классификация — публичные данные без привязки к пользователю и подключению;
|
||||||
|
её поля обновляются в том же обходе не чаще раза в сутки
|
||||||
|
([ADR 022](../adr/022-stored-instrument-classification.md)).
|
||||||
|
Подробности — [модель свежести](cross-cutting/data-freshness.md).
|
||||||
|
|
||||||
Брокерские счета, сводки, портфель, страницы журнала и будущие события
|
PUT подключения сначала проверяет token у T-Bank, затем одной транзакцией
|
||||||
не являются долговременной историей в SQLite. Они получаются для текущего
|
сохраняет encrypted token, новую revision и проверенный membership.
|
||||||
подключения и могут временно кэшироваться. Перезапуск backend не
|
Сетевая операция не удерживает SQLite-транзакцию. Общие источники не содержат
|
||||||
восстанавливает прежние финансовые снимки и cursor continuations из базы;
|
личных позиций, ожидаемых сумм или credentials. Произвольного архива
|
||||||
[модель свежести](cross-cutting/data-freshness.md) отделяет это от хранения
|
отключённых счетов нет.
|
||||||
учётных данных.
|
|
||||||
|
|
||||||
## Сохранение и восстановление данных
|
## Сохранение и восстановление данных
|
||||||
|
|
||||||
@@ -185,9 +203,8 @@ access token и вращаемый refresh token. [Политика query-кэш
|
|||||||
|
|
||||||
Авторизация доступа, недопущение token в публичный ответ и атомарность
|
Авторизация доступа, недопущение token в публичный ответ и атомарность
|
||||||
ротации сессии — отдельные свойства; наличие типизированного клиента
|
ротации сессии — отдельные свойства; наличие типизированного клиента
|
||||||
не доказывает их. Известные расхождения регистрации и деталей валидации
|
не доказывает их. Действующий формат ошибок описан в
|
||||||
отмечены в [аутентификации](../domains/authentication/overview.md#регистрация-и-вход)
|
[HTTP API](http-api.md#error-contract).
|
||||||
и [HTTP API](http-api.md#error-contract).
|
|
||||||
|
|
||||||
Поддерживается локальный Compose и ограниченная single-server topology с SQLite.
|
Поддерживается локальный Compose и ограниченная single-server topology с SQLite.
|
||||||
Origin браузера, адрес API и cookie policy должны быть согласованы.
|
Origin браузера, адрес API и cookie policy должны быть согласованы.
|
||||||
@@ -196,7 +213,8 @@ Origin браузера, адрес API и cookie policy должны быть
|
|||||||
режимах refresh-cookie имеет Secure flag и требует защищённого соединения.
|
режимах refresh-cookie имеет Secure flag и требует защищённого соединения.
|
||||||
Single-server deployment не задаёт гарантий HA или zero-downtime.
|
Single-server deployment не задаёт гарантий HA или zero-downtime.
|
||||||
|
|
||||||
Кэши и лимитер T-Bank действуют на экземпляр backend. Ротация ключа шифрования
|
Coordinator и лимитер T-Bank действуют на один экземпляр backend. Сохранённые
|
||||||
|
данные переживают перезапуск; processRunId не является distributed lease. Ротация ключа шифрования
|
||||||
подключений и согласование изменений подключения между распределёнными
|
подключений и согласование изменений подключения между распределёнными
|
||||||
экземплярами не входят в реализованный механизм. Эти ограничения не
|
экземплярами не входят в реализованный механизм. Эти ограничения не
|
||||||
позволяют переносить локальную схему на несколько экземпляров без отдельного
|
позволяют переносить локальную схему на несколько экземпляров без отдельного
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ WealthLedger нормализует их для просмотра и сопос
|
|||||||
информацией об инструментах. [Интеграция T-Bank](../integrations/tbank.md)
|
информацией об инструментах. [Интеграция T-Bank](../integrations/tbank.md)
|
||||||
объясняет полномочия подключения и ограничения внешней зависимости.
|
объясняет полномочия подключения и ограничения внешней зависимости.
|
||||||
|
|
||||||
[MOEX ISS](../integrations/moex.md) поставляет публичные параметры и календарь выпусков облигаций.
|
[MOEX ISS](../integrations/moex.md) поставляет публичные параметры и календарь выпусков облигаций,
|
||||||
|
а также дневные закрытия индексов Мосбиржи для сравнения результата.
|
||||||
[Реестр НКР](../integrations/nkr.md) используется для поиска кредитного рейтинга конкретного выпуска;
|
[Реестр НКР](../integrations/nkr.md) используется для поиска кредитного рейтинга конкретного выпуска;
|
||||||
рейтинг эмитента не подставляется вместо него. Отсутствие подходящей записи
|
рейтинг эмитента не подставляется вместо него. Отсутствие подходящей записи
|
||||||
и недоступность источника имеют разный смысл. Правила интерпретации этих
|
и недоступность источника имеют разный смысл. Правила интерпретации этих
|
||||||
|
|||||||
@@ -1,72 +1,98 @@
|
|||||||
# Свежесть данных и повторное использование
|
# Свежесть данных и повторное использование
|
||||||
|
|
||||||
WealthLedger показывает финансовое наблюдение на момент получения данных.
|
WealthLedger читает личные инвестиционные данные из SQLite. Обычный GET и его
|
||||||
Временный кэш уменьшает повторные обращения к брокеру, но не превращает
|
HTTP retry возвращают сохранённый результат и не запускают личную работу с
|
||||||
снимки в историю портфеля и не обещает рыночные данные в реальном времени.
|
T-Bank. Нормальным владельцем обновления служит часовой обход текущего
|
||||||
|
подключения: после успешного подключения он начинает первую попытку, затем
|
||||||
|
начинает следующую не ранее чем через 60 минут от начала предыдущей. После
|
||||||
|
простоя выполняется не более одна актуальная попытка, а перекрывающийся срок
|
||||||
|
пропускается, не ставится в очередь. Общие справочные данные инструментов
|
||||||
|
запрашиваются онлайн, когда это требуется карточке; финансовый HTTP-ответ не
|
||||||
|
ждёт T-Bank. Единственное исключение — сектор и бренд актива позиции, которые
|
||||||
|
хранятся как последнее известное значение с обновлением не чаще раза в сутки;
|
||||||
|
свежесть и полнота Portfolio от них не зависят. Модель принята в
|
||||||
|
[ADR 019](../../adr/019-hourly-reads-and-online-instrument-reference.md),
|
||||||
|
исключение — в
|
||||||
|
[ADR 022](../../adr/022-stored-instrument-classification.md).
|
||||||
|
|
||||||
## Клиентский и серверный снимки
|
## Клиентский и серверный снимки
|
||||||
|
|
||||||
[Client Cached Snapshot](../../glossary.md#client-cached-snapshot) — последний
|
[Client Cached Snapshot](../../glossary.md#client-cached-snapshot) — успешный
|
||||||
успешный ответ, сохранённый TanStack Query в SPA. При обычном фоновом обновлении
|
ответ в TanStack Query. Он сохраняет показанное содержимое при временной ошибке
|
||||||
того же набора данных уже показанный результат остаётся на экране; ошибка
|
HTTP-перечитывания в той же границе доступа. Это не серверный fallback и не
|
||||||
обновления сообщает о проблеме, не заменяя известные значения пустотой.
|
состояние часового обхода. SQLite независимо от браузера хранит membership,
|
||||||
Клиентский кэш не является внешней системой или отдельным runtime-приложением.
|
последний полный portfolio, исполненные операции, прогнозы, общие сведения об
|
||||||
|
инструментах и классификацию инструментов (сектор и бренд актива как
|
||||||
|
последнее известное значение, [ADR
|
||||||
|
022](../../adr/022-stored-instrument-classification.md)), котировки, свечи и
|
||||||
|
календари.
|
||||||
|
|
||||||
[Portfolio Snapshot](../../glossary.md#portfolio-snapshot) — полный результат
|
[Полнота периода](../../glossary.md#полнота-истории-счёта) и свежесть различаются.
|
||||||
загрузки портфеля на backend. В пределах разрешённого сценарием срока он может
|
Успешно проверенный пустой результат полон и доступен; последняя страница
|
||||||
служить серверным fallback при временной ошибке upstream. Backend отмечает
|
журнала сама по себе полноту не доказывает. Результат каждого личного ресурса
|
||||||
такой ответ `isStale`; frontend не выводит это поле из состояния собственного
|
в `{ data, meta }` описывают пять независимых значений: freshness,
|
||||||
query-кэша. Серверное предупреждение и ошибка клиентского refetch могут
|
completeness, `lastSuccessfulAt`, `lastAttempt` и availability. Первые,
|
||||||
возникать независимо.
|
полнота и `lastSuccessfulAt` относятся только к возвращённому подтверждённому
|
||||||
|
результату. `lastAttempt` описывает относящуюся к ресурсу часовую попытку:
|
||||||
|
`none`, `running`, `succeeded`, `partial` или `failed`.
|
||||||
|
|
||||||
Свежесть и допустимость fallback определяются сценарием. Портфельный fallback
|
Итог попытки выводится из фактических результатов её шагов: попытка, не
|
||||||
не распространяется автоматически на журнал, прогноз событий или страницу
|
завершившая обязательный шаг, не обозначается полным успехом, а полученные
|
||||||
инструмента. Точные правила находятся в
|
успешные результаты сохраняются. Ожидание каждого шага ограничено; зависший
|
||||||
[справочнике портфеля](../../domains/portfolio/data-reference.md),
|
шаг учитывается проблемным, а начатое обращение к T-Bank не отменяется.
|
||||||
[журнале операций](../../domains/operations/overview.md) и
|
Попытка, потерявшая выполнение после сбоя сохранения или остановки процесса,
|
||||||
[будущих событиях](../../domains/income/upcoming-events.md).
|
закрывается частичным успехом либо неуспехом и не блокирует следующий обход
|
||||||
|
дольше обычного часового срока.
|
||||||
|
|
||||||
По [ADR 007](../../adr/007-react-query-fetch-policy.md) запрос связан с открытием
|
Поздняя неуспешная или частичная попытка сохраняет прежний полный результат и
|
||||||
экрана, а возврат фокуса окна не запускает refetch. Повторное открытие после
|
его исходное время. Если полного результата нет, `data` равно `null`: состояние
|
||||||
`staleTime` может обновить ответ в фоне. Эта политика не является таймером
|
availability объясняет первоначальную загрузку, временную недоступность либо
|
||||||
периодического обновления. Перезапуск backend уничтожает его in-memory
|
недоступность подключения. Пустой массив не означает отсутствие результата.
|
||||||
снимки и continuations журнала; SQLite не восстанавливает их.
|
Старый полный portfolio не удаляется из-за возраста. `Portfolio.isStale`
|
||||||
|
сохраняет смысл возраста его snapshot, а не обозначает неуспех обхода или
|
||||||
|
клиентскую ошибку перечитывания.
|
||||||
|
|
||||||
|
| Источник | Владелец нормального обновления |
|
||||||
|
| --- | --- |
|
||||||
|
| Membership, portfolio, изменяемый хвост операций и события | Часовой обход текущего подключения |
|
||||||
|
| Котировки и изменяемый хвост дневных свечей | Установленная политика источника; не ordinary personal GET |
|
||||||
|
| Metadata, поиск каталога и company | Онлайн при чтении |
|
||||||
|
| Полные прошлые интервалы операций и завершённые свечи | Не обновляются часовым обходом или обычным GET |
|
||||||
|
|
||||||
|
Два исключения разрешают только первое получение отсутствующего результата:
|
||||||
|
первое открытие неподтверждённого исторического интервала операций получает
|
||||||
|
только этот интервал и возвращает локальные строки с неполным покрытием; первое
|
||||||
|
открытие диапазона свечей без подтверждённого локального результата получает
|
||||||
|
только этот диапазон. Ни одно исключение не обновляет membership, portfolio,
|
||||||
|
события, другой интервал или диапазон. Их HTTP retry также только перечитывает
|
||||||
|
локальное состояние. Подтверждённый полный интервал или диапазон остаётся
|
||||||
|
локальным.
|
||||||
|
|
||||||
|
Часовой обход сохраняет успешные разделы независимо, а failed section не
|
||||||
|
заменяет прежний complete result. Отменой query lifecycle владеет TanStack
|
||||||
|
Query. Открытый экран перечитывает ресурс не чаще одного раза в 30 минут;
|
||||||
|
это локальный HTTP read. Совместимый параметр `sync=observe`, пока он есть в
|
||||||
|
маршруте, также является только чтением и не представляет команду обходу.
|
||||||
|
[HTTP API](../http-api.md) описывает исполняемый контракт envelope.
|
||||||
|
|
||||||
## Смена подключения
|
## Смена подключения
|
||||||
|
|
||||||
Кэш финансовых данных относится к пользователю, счёту и текущему
|
Каждое чтение проверяет локальную сессию, текущее подключение, известный отказ
|
||||||
подключению T-Bank. Backend сначала читает подключение и его generation, затем
|
токена и сохранённый состав доступных счетов. Расшифровка token и GetAccounts
|
||||||
проверяет возможность повторного использования ответа. Свежий портфель
|
для локальной выдачи не нужны. Старый membership допускает выдачу и фоновую
|
||||||
одного счёта не требует нового запроса списка счетов у брокера; при обновлении
|
сверку; это задержка обнаружения отзыва доступа, а не live-проверка брокера.
|
||||||
портфеля доступ проверяется заново. Журнал операций проверяет доступ до чтения
|
Unknown membership не даёт доступа к произвольному accountId: ответ сообщает
|
||||||
кэшированной страницы. Идентификатор счёта и совпадение параметров запроса
|
о загрузке доступа. Успешный новый список немедленно ограничивает последующие
|
||||||
сами по себе недостаточны для выдачи кэша.
|
чтения. Известный invalid token блокирует личные данные.
|
||||||
|
|
||||||
При каждом успешном сохранении подключения token шифруется с новым
|
Успешный PUT атомарно сохраняет token, новую случайную внутреннюю revision
|
||||||
AES-GCM nonce. Из сохранённого шифротекста backend вычисляет непрозрачную
|
и membership из проверки token. Прежние личные datasets и work становятся
|
||||||
внутреннюю generation. Она не хранится отдельным полем базы и не входит
|
недоступны. DELETE удаляет подключение и личные данные; общие неперсональные
|
||||||
в API, журналы или ошибки. Portfolio Snapshot и Ledger Page Cache допускают
|
источники могут оставаться. Неуспешный PUT сохраняет действующую границу.
|
||||||
повторное использование только в пределах соответствующей generation.
|
|
||||||
Старый кэш или поздно завершившийся запрос прежней generation не обслуживает
|
|
||||||
новый запрос после замены подключения, включая сохранение того же plaintext
|
|
||||||
token. Удаление подключения не оставляет доступного контекста для чтения кэша.
|
|
||||||
|
|
||||||
После успешного изменения подключения frontend сначала записывает
|
Публикация проверяет revision и attempt в той же SQLite-транзакции; поздняя
|
||||||
подтверждённый status, затем сбрасывает generated GET queries в поддеревьях
|
работа старого подключения не меняет даже timestamp. Revision не попадает
|
||||||
`/api/v1/brokerage-accounts`, `/api/v1/stocks` и `/api/v1/bonds`. Активные
|
в API, логи или cursor. Frontend очищает personal queries при смене подключения
|
||||||
выборки запрашиваются заново, неактивные — при следующем использовании.
|
и известном отказе доступа; поздний ответ не восстанавливает скрытые данные.
|
||||||
Client Cached Snapshot прежнего подключения больше не служит результатом
|
Logout очищает сессию и query cache. Подробнее —
|
||||||
новой границы.
|
[интеграция T-Bank](../../integrations/tbank.md).
|
||||||
|
|
||||||
Неуспешная операция подключения не меняет статус, generation и доступные
|
|
||||||
финансовые данные. Обычная ошибка refetch также сохраняет клиентский снимок
|
|
||||||
текущей границы. Logout — отдельное событие: оно очищает состояние
|
|
||||||
пользовательской сессии, status подключения и query cache.
|
|
||||||
|
|
||||||
Пользовательский сценарий описан в [интеграции T-Bank](../../integrations/tbank.md).
|
|
||||||
Согласованные правила frontend находятся в workspace-спецификациях
|
|
||||||
[query coherence](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/frontend-query-coherence/spec.md)
|
|
||||||
и [data resilience](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/frontend-data-resilience/spec.md);
|
|
||||||
серверные правила — в
|
|
||||||
[portfolio](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md) и
|
|
||||||
[ledger](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-ledger/spec.md).
|
|
||||||
|
|||||||
@@ -16,20 +16,48 @@
|
|||||||
пока неизвестно, доступен public или protected маршрут. При загрузке
|
пока неизвестно, доступен public или protected маршрут. При загрузке
|
||||||
отложенного маршрута уже доступный каркас остаётся видимым.
|
отложенного маршрута уже доступный каркас остаётся видимым.
|
||||||
|
|
||||||
Фоновое обновление и действие пользователя сохраняют видимые данные или
|
Фоновое чтение не сообщает о себе: оно сохраняет видимые данные без индикации
|
||||||
элемент управления с компактной индикацией процесса. В частности,
|
процесса и без технического предупреждения. В частности,
|
||||||
[клиентский снимок](data-freshness.md#клиентский-и-серверный-снимки) счетов или
|
[клиентский снимок](data-freshness.md#клиентский-и-серверный-снимки) счетов или
|
||||||
портфеля остаётся доступным после ошибки refetch; сообщение и повтор
|
портфеля остаётся доступным после ошибки refetch, и эта ошибка не означает
|
||||||
относятся к обновлению, а не к отсутствию инвестиций.
|
отсутствие инвестиций. Действие пользователя сохраняет видимые данные или
|
||||||
|
элемент управления и сообщает свой результат, включая неуспешный.
|
||||||
|
|
||||||
|
Первичное ожидание ограничено по времени. По его истечении область переходит
|
||||||
|
в спокойную локальную недоступность вместо бесконечной загрузки и сама
|
||||||
|
заменяет её содержимым после восстановления. Решение принято в
|
||||||
|
[ADR 021](../../adr/021-quiet-reading-and-rare-confirmations.md).
|
||||||
|
|
||||||
|
Серверная полнота не выводится из query lifecycle. Missing показывает
|
||||||
|
skeleton только отсутствующих секций, partial — подтверждённые строки и
|
||||||
|
сообщение о неполноте. Totals ждут обязательных зависимостей, comparisons
|
||||||
|
готовятся независимо. Только complete empty означает отсутствие операций.
|
||||||
|
Время источника не заменяется временем получения HTTP-ответа.
|
||||||
|
|
||||||
|
Экраны просмотра не предлагают ручное обновление или повтор чтения: временные
|
||||||
|
сбои GET восстанавливаются сами ограниченным числом повторов, сохраняя
|
||||||
|
показанный результат и applied filters. Ошибка transport отличается от
|
||||||
|
ошибки source work. Status observation прекращается после terminal outcome
|
||||||
|
или ухода; само истечение TTL не запускает импорт. Известный access loss
|
||||||
|
очищает personal view, включая защиту от позднего ответа.
|
||||||
|
|
||||||
|
Состояние источника при этом видимо: время последнего успешного результата,
|
||||||
|
устаревание, исход последней часовой попытки и неполнота показаны текстом либо
|
||||||
|
доступно именованным элементом рядом с данными, к которым относятся. Это
|
||||||
|
свойство данных, а не отчёт о техническом процессе, и оно не требует действия.
|
||||||
|
|
||||||
## Известная ошибка сценария
|
## Известная ошибка сценария
|
||||||
|
|
||||||
Ошибки счетов, портфеля и подключения T-Bank остаются локальными: рядом
|
Ошибки счетов, портфеля и подключения T-Bank остаются локальными: затронутый
|
||||||
с затронутыми данными доступны предусмотренные сценарием retry и navigation
|
блок показывает собственное состояние и не скрывает успешно отображённые
|
||||||
actions. Устаревший серверный Portfolio Snapshot показывает сохранённый
|
соседние блоки. Устаревший серверный Portfolio Snapshot показывает сохранённый
|
||||||
портфель с предупреждением. Он не превращается в глобальный fallback.
|
портфель с обозначением устаревания. Он не превращается в глобальный fallback.
|
||||||
Ошибка источника не означает нулевой баланс или отсутствие операций.
|
Ошибка источника не означает нулевой баланс или отсутствие операций.
|
||||||
|
|
||||||
|
Предусмотренные сценарием navigation actions сохраняются: подтверждённая потеря
|
||||||
|
доступа предлагает восстановить подключение, а `404` — вернуться к списку
|
||||||
|
счетов. Это навигация и явные действия пользователя, а не повтор чтения.
|
||||||
|
|
||||||
## Непойманная ошибка маршрута
|
## Непойманная ошибка маршрута
|
||||||
|
|
||||||
Непойманная runtime-ошибка при отображении public или protected route
|
Непойманная runtime-ошибка при отображении public или protected route
|
||||||
|
|||||||
+105
-148
@@ -1,30 +1,41 @@
|
|||||||
# Контейнерная поставка
|
# Контейнерная поставка
|
||||||
|
|
||||||
Runbook для одного Docker host, одного backend и краткого простоя.
|
Поставка для одного Docker host, одного backend и краткого простоя.
|
||||||
[Топология](containers.md#контейнерная-поставка) и
|
[Топология](containers.md#контейнерная-поставка) и
|
||||||
[ADR 015](../adr/015-single-server-sqlite-deployment.md) объясняют границы.
|
[ADR 015](../adr/015-single-server-sqlite-deployment.md) объясняют границы.
|
||||||
Команды выполняются из checkout `WealthLedger/main`, содержащего `compose.yaml`.
|
Production-комплект находится в отдельном [репозитории deploy](https://git.personal.ksv741.ru/WealthLedger/deploy).
|
||||||
Development дополнительно требует checkout `frontend/` и `backend/`.
|
Его README — единственная пошаговая инструкция установки, обновления и recovery;
|
||||||
|
[ADR 016](../adr/016-versioned-deployment-bundle.md) объясняет выбор релиза комплекта.
|
||||||
|
Development-команды ниже выполняются из workspace с checkout `frontend/` и `backend/`.
|
||||||
|
|
||||||
## Предпосылки
|
## Предпосылки
|
||||||
|
|
||||||
- Docker Engine с Compose plugin **не ниже 2.24.4** (`!override` в development),
|
- Docker Engine с Compose plugin **не ниже 2.24.4** (`!override` в development),
|
||||||
доступ к registry и достаточно свободного диска для non-root UID 1000.
|
доступ к registry и достаточно свободного диска для non-root UID 1000.
|
||||||
Локально проверены Engine 29.8.0, Compose 5.5.1, Linux arm64.
|
Опубликованные images собраны для Linux amd64 (см. README комплекта).
|
||||||
Другую архитектуру проверяют отдельно; multi-platform images не обещаны.
|
Другую архитектуру проверяют отдельно; multi-platform images не обещаны.
|
||||||
- Gitea Container Registry; проверяемая версия сервера — 1.27.3.
|
- Nexus Docker hosted repository `docker-hosted`, доступный через
|
||||||
|
`nexus.personal.ksv741.ru/docker-hosted/`. Docker Bearer Token Realm включён;
|
||||||
|
reverse proxy передаёт исходные host и HTTPS scheme. Адрес token realm
|
||||||
|
в `WWW-Authenticate` также должен начинаться с `https://`.
|
||||||
|
Deployment policy запрещает redeploy существующих release tags.
|
||||||
Runner label — `ubuntu-latest`, runner должен иметь Docker CLI, buildx и daemon.
|
Runner label — `ubuntu-latest`, runner должен иметь Docker CLI, buildx и daemon.
|
||||||
Наличие label само по себе не доказывает Docker capability.
|
Наличие label само по себе не доказывает Docker capability.
|
||||||
- В Actions каждого component repo нужны `REGISTRY_USERNAME` и
|
- В Actions каждого component repo нужны `NEXUS_USERNAME` и
|
||||||
`REGISTRY_TOKEN` отдельной identity с package write permission. Server
|
`NEXUS_PASSWORD` пользователя `wealthledger-ci`. Эта же учётная запись
|
||||||
использует другую identity только с package read permission. Built-in
|
используется сервером для загрузки образов. Для публикации нужны
|
||||||
workflow token не заменяет эту настройку.
|
browse/read/add/edit на `docker-hosted`; фактически выданный набор прав
|
||||||
|
настраивается в Nexus. Built-in
|
||||||
|
workflow token не заменяет эту настройку. Отдельной read-only учётной записи
|
||||||
|
сервера нет: `wealthledger-ci` работает и как CI writer, и как server reader,
|
||||||
|
поэтому её пароль на сервере даёт право публикации в `docker-hosted`. Это
|
||||||
|
принятое владельцем решение; разделение ролей — возможное усиление.
|
||||||
- На сервере Caddy уже завершает TLS и передаёт весь домен на
|
- На сервере Caddy уже завершает TLS и передаёт весь домен на
|
||||||
`127.0.0.1:8080` (или выбранный frontend port). Отдельного `/api` upstream нет.
|
`127.0.0.1:8080` (или выбранный frontend port). Отдельного `/api` upstream нет.
|
||||||
DNS, TLS, firewall и provisioning Caddy выполняются вне этого runbook.
|
DNS, TLS, firewall и provisioning Caddy выполняются вне этого runbook.
|
||||||
|
|
||||||
Реальная публикация и server pull-only login требуют отдельной проверки
|
Серверный login и доступ к images проверяются на целевом сервере.
|
||||||
на вашей инфраструктуре. Локальные images не доказывают registry permissions.
|
Локальный smoke не доказывает серверные registry permissions.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -63,7 +74,7 @@ Project `wealthledger-dev` хранит SQLite в `development-data`. Обычн
|
|||||||
version, затем выполняет `npm ci`, `npm run verify`; backend дополнительно
|
version, затем выполняет `npm ci`, `npm run verify`; backend дополнительно
|
||||||
выполняет `npm run test:e2e`. У каждого image OCI version/revision labels.
|
выполняет `npm run test:e2e`. У каждого image OCI version/revision labels.
|
||||||
|
|
||||||
Images публикуются как `git.personal.ksv741.ru/wealthledger/frontend:X.Y.Z`
|
Images публикуются как `nexus.personal.ksv741.ru/docker-hosted/wealthledger/frontend:X.Y.Z`
|
||||||
и `.../backend:X.Y.Z`. `latest` не создаётся.
|
и `.../backend:X.Y.Z`. `latest` не создаётся.
|
||||||
Workflow отказывается перезаписывать существующие release tags;
|
Workflow отказывается перезаписывать существующие release tags;
|
||||||
защита release tags и запрет параллельных publishers обязательны. Если одна
|
защита release tags и запрет параллельных publishers обязательны. Если одна
|
||||||
@@ -72,156 +83,102 @@ Workflow отказывается перезаписывать существу
|
|||||||
|
|
||||||
Выберите опубликованные version tags, выполните pull и сравните OCI revision
|
Выберите опубликованные version tags, выполните pull и сравните OCI revision
|
||||||
с release commit. Выполните smoke с disposable data/config до первого server
|
с release commit. Выполните smoke с disposable data/config до первого server
|
||||||
rollout. В production env хранится пара полных image references, например
|
rollout. В `release.env` комплекта хранится пара полных image references, например
|
||||||
`git.personal.ksv741.ru/wealthledger/frontend:0.1.0`. Сохраните versions/revisions
|
`nexus.personal.ksv741.ru/docker-hosted/wealthledger/frontend:1.1.0` и
|
||||||
|
`.../backend:2.1.0`. Сохраните versions/revisions
|
||||||
в deployment record. Release tags не должны переназначаться другим images.
|
в deployment record. Release tags не должны переназначаться другим images.
|
||||||
|
|
||||||
|
## Перенос существующих images из Gitea
|
||||||
|
|
||||||
|
До переключения CI и server references проверьте HTTPS token realm и настройте
|
||||||
|
пользователя `wealthledger-ci`. В Actions добавьте `NEXUS_USERNAME`/`NEXUS_PASSWORD`; прежние
|
||||||
|
`REGISTRY_USERNAME`/`REGISTRY_TOKEN` сохраните для отката старого workflow.
|
||||||
|
Пароли вводятся интерактивно или через `--password-stdin`, не через аргументы.
|
||||||
|
|
||||||
|
Прежние images лежат в [Gitea packages](https://git.personal.ksv741.ru/WealthLedger/-/packages)
|
||||||
|
(`git.personal.ksv741.ru/wealthledger/{frontend,backend}`); новые публикуются
|
||||||
|
только в Nexus. Скопируйте каждый опубликованный release без пересборки. Например, для `0.2.0`
|
||||||
|
с установленным Skopeo и доступом чтения Gitea/записи Nexus:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
skopeo login git.personal.ksv741.ru
|
||||||
|
skopeo login nexus.personal.ksv741.ru
|
||||||
|
for component in frontend backend; do
|
||||||
|
skopeo copy --all --preserve-digests \
|
||||||
|
"docker://git.personal.ksv741.ru/wealthledger/$component:0.2.0" \
|
||||||
|
"docker://nexus.personal.ksv741.ru/docker-hosted/wealthledger/$component:0.2.0" || exit 1
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
`--all` переносит весь image index, включая платформы и вложенные attestation
|
||||||
|
manifests; `--preserve-digests` прерывает перенос при изменении digest.
|
||||||
|
До копирования проверьте наличие destination tag: если он уже существует,
|
||||||
|
сравните исходный и целевой index digest и вложенные manifests. Совпадающий
|
||||||
|
artifact повторно не публикуйте; несовпадение требует расследования, не удаления
|
||||||
|
или перезаписи tag. Частичный перенос не считается завершённым.
|
||||||
|
|
||||||
|
После копирования сравните digests через `skopeo inspect --raw` и SHA-256,
|
||||||
|
выполните `docker pull` обоих images пользователем `wealthledger-ci` на сервере приложения,
|
||||||
|
проверьте OCI revision и container smoke на disposable data.
|
||||||
|
Серверные шаги (login, pull, smoke) в репозитории не подтверждены и
|
||||||
|
выполняются оператором. Только после них меняйте
|
||||||
|
production image references и запускайте обычную
|
||||||
|
последовательность обновления ниже. Старые Gitea packages и references сохраните:
|
||||||
|
откат registry возвращает прежние адреса тех же artifacts, не восстанавливает
|
||||||
|
SQLite и не меняет encryption keys. Не переносите Git release tags и не
|
||||||
|
запускайте заново старый tag workflow для копирования существующего image.
|
||||||
|
|
||||||
## Первая установка
|
## Первая установка
|
||||||
|
|
||||||
Создайте restricted environment file и data directory:
|
На подготовленном сервере оператор клонирует `deploy`, настраивает внешний
|
||||||
|
HTTPS и registry login, размещает server env, сертификат T-Bank и data directory
|
||||||
|
вне checkout. Для новой пустой базы команда вызывается с `--init`.
|
||||||
|
Для действующей установки сохраняются прежние данные и ключи, а первый переход
|
||||||
|
выполняется как полное обновление с backup. Отсутствующая БД без `--init` — ошибка.
|
||||||
|
Точные команды и разрешения каталогов приведены в README комплекта.
|
||||||
|
|
||||||
```bash
|
Git-тег `deploy` закрепляет пару images через `release.env`. Версия комплекта
|
||||||
sudo install -d -m 0700 /srv/wealthledger
|
не обязана совпадать с версиями компонентов. Публикация image не выпускает
|
||||||
sudo install -d -m 0700 -o 1000 -g 1000 /srv/wealthledger/data
|
комплект и не обновляет сервер автоматически. Команда получает выбранный тег,
|
||||||
sudo install -m 0600 .env.compose.example /srv/wealthledger/production.env
|
загружает images до простоя и использует весь snapshot этого commit.
|
||||||
```
|
|
||||||
|
|
||||||
Дальнейшие команды выполняет оператор, имеющий доступ к environment file,
|
|
||||||
backup directory и Docker. Заполните env локальным защищённым редактором:
|
|
||||||
полные ссылки `FRONTEND_IMAGE` и `BACKEND_IMAGE` с опубликованными version tags,
|
|
||||||
публичный HTTPS origin и абсолютный data path. Версии в примере — образец
|
|
||||||
формата; перед запуском выберите реально опубликованные releases.
|
|
||||||
Пустые required значения блокируют config; некорректные secrets блокируют backend.
|
|
||||||
Не выводите полный rendered config с настоящими secrets в журналы.
|
|
||||||
|
|
||||||
Для новой базы создайте JWT secret длиной хотя бы 32 символа и отдельный
|
|
||||||
32-byte encryption key в base64 (`openssl rand -hex 32` и
|
|
||||||
`openssl rand -base64 32` в защищённом терминале). Сохраните ключ отдельно
|
|
||||||
от data backup. Для существующей установки перенесите прежние JWT secret
|
|
||||||
и encryption key, а согласованную копию **всего** data directory — при
|
|
||||||
остановленном прежнем backend. Файл должен называться `wealthledger.db`;
|
|
||||||
если переименовываете, согласуйте также WAL/SHM companion names до старта.
|
|
||||||
Не запускайте старый и новый backend на одной базе одновременно.
|
|
||||||
|
|
||||||
Войдите в registry server identity без помещения token в командную строку:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker login git.personal.ksv741.ru --username SERVER_PULL_USER
|
|
||||||
export COMPOSE_FILE=compose.yaml
|
|
||||||
export COMPOSE_PROJECT_NAME=wealthledger
|
|
||||||
export COMPOSE_ENV_FILES=/srv/wealthledger/production.env
|
|
||||||
docker compose config --quiet
|
|
||||||
docker compose pull frontend backend migration
|
|
||||||
docker compose up -d --wait --wait-timeout 120
|
|
||||||
docker compose ps -a
|
|
||||||
```
|
|
||||||
|
|
||||||
Migration и backend используют один image и один data mount.
|
|
||||||
Migration должна завершиться с кодом 0; иначе backend не запускается.
|
|
||||||
Readiness принимает 401 от существующего `/api/v1/auth/me` без credentials.
|
|
||||||
Frontend становится healthy после backend. Backend host port отсутствует.
|
|
||||||
Runtime non-root; root filesystem read-only, nginx temp files находятся в tmpfs.
|
|
||||||
|
|
||||||
Проверьте SPA deep link, регистрацию/вход, refresh, существующие карты,
|
|
||||||
чтение старого зашифрованного подключения и один финансовый запрос.
|
|
||||||
Production refresh-cookie имеет Secure flag: проверяйте через HTTPS Caddy;
|
|
||||||
loopback HTTP curl без browser cookie jar не доказывает browser session.
|
|
||||||
При локальном smoke используйте disposable credentials и явную передачу cookie.
|
|
||||||
Направляйте Caddy на новый deployment только после успешной проверки.
|
|
||||||
Прежний runtime удаляйте только после проверки новой установки.
|
|
||||||
|
|
||||||
## Backup при остановленном backend
|
## Backup при остановленном backend
|
||||||
|
|
||||||
До backend update проверьте совместимость новой версии с текущим frontend
|
Сначала останавливаются писатели, затем копируется весь каталог данных,
|
||||||
и схемой, а обратной версии — с новой схемой. При неизвестной совместимости
|
включая возможные WAL/SHM. Только успешная копия допускает миграции. Локальные
|
||||||
не обещайте image rollback. В выбранном rollout отсутствуют параллельные API.
|
backups находятся рядом с data directory, вне checkout, и автоматически
|
||||||
|
не удаляются. Незавершённая копия помечается `.partial-` и не подходит для restore.
|
||||||
|
Ключ шифрования хранится отдельно. Локальная копия не защищает от потери диска.
|
||||||
|
|
||||||
Ниже data path соответствует примеру env; при другом path замените его.
|
## Единый цикл обновления
|
||||||
Сохраните текущую пару image references/revisions и отдельно — ключи.
|
|
||||||
|
|
||||||
```bash
|
Каждое обновление, включая повтор того же тега и изменение только frontend,
|
||||||
docker compose stop backend
|
выполняет stop → backup → migration → readiness. Сравнение hashes контейнеров
|
||||||
docker compose ps -a backend
|
и конфигурации не выполняется. Приняты короткий простой API и дополнительный
|
||||||
install -d -m 0700 /srv/wealthledger/backups
|
backup; во время простоя возможен 502, вкладкам может потребоваться reload.
|
||||||
BACKUP_DIRECTORY="/srv/wealthledger/backups/$(date -u +%Y%m%dT%H%M%SZ)"
|
Ошибка migration блокирует старт, ошибка готовности останавливает новый backend;
|
||||||
mkdir -m 0700 "$BACKUP_DIRECTORY"
|
ошибка frontend не останавливает исправный backend. Успех фиксируется только
|
||||||
cp -a /srv/wealthledger/data "$BACKUP_DIRECTORY/data"
|
после проверок запуска обоих компонентов.
|
||||||
```
|
|
||||||
|
|
||||||
Перед копированием убедитесь, что backend остановлен, migration не выполняется
|
Одна file lock защищает установку от параллельных команд. Маркер незавершённого
|
||||||
и никто другой не пишет в каталог. Успех `cp` обязателен перед продолжением.
|
обновления блокирует повтор до разбора оператором. Он содержит этап, версии
|
||||||
Не копируйте отдельный `.db` из работающего приложения. Проверяйте backup
|
и путь к backup, но не секреты и не копию данных. После обновления нужен smoke
|
||||||
на отдельной копии: migration status, `PRAGMA integrity_check`, auth, карты
|
через HTTPS: healthcheck сам по себе не доказывает работу Secure refresh-cookie.
|
||||||
и расшифровка тестового подключения исходным ключом. Backup без проверяемого
|
|
||||||
ключа недостаточен для восстановления подключения.
|
|
||||||
|
|
||||||
## Независимое обновление frontend
|
|
||||||
|
|
||||||
После проверки API compatibility замените только `FRONTEND_IMAGE` в env:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose pull frontend
|
|
||||||
docker compose up -d --no-deps --wait --wait-timeout 120 frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
Backend container и SQLite не пересоздаются. Входная точка кратко недоступна.
|
|
||||||
Проверьте deep link, login/refresh и новую OCI revision. Старым browser tabs
|
|
||||||
может понадобиться reload: старые hashed chunks не сохраняются между images.
|
|
||||||
|
|
||||||
## Независимое обновление backend
|
|
||||||
|
|
||||||
Выполните stopped-backend backup выше. Замените только `BACKEND_IMAGE` в env.
|
|
||||||
Последовательность не продолжается после failed migration:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose pull backend migration
|
|
||||||
docker compose up --no-deps --force-recreate --abort-on-container-exit \
|
|
||||||
--exit-code-from migration migration &&
|
|
||||||
docker compose up -d --no-deps --wait --wait-timeout 120 backend
|
|
||||||
```
|
|
||||||
|
|
||||||
Frontend не пересоздаётся; nginx повторно разрешает backend DNS. Во время
|
|
||||||
перерыва возможен 502. При failure оставьте backend остановленным, сохраните
|
|
||||||
диагностику migration и выберите совместимый image либо отдельный restore.
|
|
||||||
Не используйте `migrate reset` или автоматический `resolve` как recovery.
|
|
||||||
Проверьте migration status, health, login/refresh, карты и расшифровку подключения.
|
|
||||||
После успешного smoke запишите фактически установленную пару image references.
|
|
||||||
|
|
||||||
## Откат image
|
## Откат image
|
||||||
|
|
||||||
Для frontend верните предыдущий совместимый version tag и повторите frontend update.
|
Откат версии и восстановление базы — разные операции. Прежний backend можно
|
||||||
Для backend сначала остановите процесс и подтвердите совместимость старого
|
запускать только после проверки совместимости с текущей схемой. Автоматического
|
||||||
image с **текущей** схемой. Верните предыдущий image reference и запустите только backend:
|
rollback нет; migration service при ручном image rollback не запускается.
|
||||||
|
Порядок ручных Compose-команд и проверки recovery находится в README комплекта.
|
||||||
```bash
|
|
||||||
docker compose pull backend
|
|
||||||
docker compose up -d --no-deps --wait --wait-timeout 120 backend
|
|
||||||
```
|
|
||||||
|
|
||||||
Данные и encryption key не заменяются. Migration service здесь намеренно
|
|
||||||
не запускается: старые migrations не являются down-migrations. Прочитайте
|
|
||||||
новые записи, сделанные после backup, и проверьте auth/cards/connection.
|
|
||||||
При несовместимости оставьте приложение остановленным до решения о recovery.
|
|
||||||
|
|
||||||
## Explicit restore
|
## Explicit restore
|
||||||
|
|
||||||
Это отдельное решение оператора, **не часть image rollback**. Оператор выбирает
|
Оператор выбирает проверенную копию и явно принимает потерю всех более новых
|
||||||
backup timestamp и явно принимает потерю всех изменений после него, включая
|
записей. Сначала восстановление проверяется на isolated данных с прежним ключом
|
||||||
сессии, карты и подключения. Сначала воспроизведите restore на isolated project
|
и совместимым image. На целевой установке останавливаются писатели, текущий
|
||||||
с отдельным data path/port и совместимым image; не используйте production data
|
каталог сохраняется под новым именем, затем восстанавливается выбранная копия.
|
||||||
как fixture. Убедитесь в наличии соответствующего encryption key.
|
Backend пересоздаётся, чтобы заново подключить заменённый bind-каталог:
|
||||||
|
простой stop/start может сохранить mount прежних данных.
|
||||||
После отдельного решения остановите backend и убедитесь, что migration не
|
Проверяются целостность БД, auth/cards и расшифровка подключения. Только после
|
||||||
работает. Сохраните нынешний каталог под новым именем, ничего не удаляя:
|
подтверждённого recovery можно снять маркер незавершённой операции.
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose stop backend
|
|
||||||
mv /srv/wealthledger/data /srv/wealthledger/data-before-restore-TIMESTAMP
|
|
||||||
cp -a /srv/wealthledger/backups/CHOSEN_TIMESTAMP/data /srv/wealthledger/data
|
|
||||||
```
|
|
||||||
|
|
||||||
Проверьте owner 1000:1000, permissions, выбранный compatible image и исходный
|
|
||||||
ключ. Запустите backend командой rollback с `--no-deps`, затем выполните smoke.
|
|
||||||
Сверьте наличие записей до backup и отсутствие известных записей потерянного
|
|
||||||
интервала. Сохраните прежний каталог для расследования. При необходимости
|
|
||||||
новой migration сначала выполните обычную backup/update последовательность.
|
|
||||||
|
|||||||
+68
-11
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
## Источник контракта
|
## Источник контракта
|
||||||
|
|
||||||
Backend генерирует OpenAPI из собственного публичного API. Frontend использует
|
Backend генерирует OpenAPI из собственного публичного API. Этот сгенерированный
|
||||||
|
OpenAPI — исполняемый источник HTTP-контракта. Frontend использует
|
||||||
предоставленный OpenAPI snapshot как актуальный вход и локально генерирует
|
предоставленный OpenAPI snapshot как актуальный вход и локально генерирует
|
||||||
типизированный клиент. Backend владеет контрактом, а подготовка и доставка
|
типизированный клиент. Backend владеет контрактом, а подготовка и доставка
|
||||||
схемы находятся вне frontend. Это разделение принято в
|
схемы находятся вне frontend. Это разделение принято в
|
||||||
@@ -20,6 +21,67 @@ artifact у себя и проверяет соответствие generated ty
|
|||||||
API имеет prefix `/api/v1`; Swagger публикуется для локальной разработки по
|
API имеет prefix `/api/v1`; Swagger публикуется для локальной разработки по
|
||||||
environment policy, а не используется frontend в runtime.
|
environment policy, а не используется frontend в runtime.
|
||||||
|
|
||||||
|
## Скринер акций
|
||||||
|
|
||||||
|
`POST /api/v1/stocks/screener/search` всегда создаёт новый результат по
|
||||||
|
условиям скринера, включая пустой подтверждённый список. Ответ первой страницы
|
||||||
|
содержит непрозрачный `searchId`, время получения каталога и признак следующей
|
||||||
|
страницы. `GET /api/v1/stocks/screener/search/{searchId}?page=...` читает
|
||||||
|
последующую страницу того же результата, не запускает новый поиск и сохраняет
|
||||||
|
состав и время списка. Идентификатор результата принадлежит текущему пользователю
|
||||||
|
и его подключению; недоступный или чужой результат возвращает ошибку, а не
|
||||||
|
пустой список. Точные поля и статусы определяет backend-generated OpenAPI.
|
||||||
|
|
||||||
|
## Инвестиционные данные и синхронизация
|
||||||
|
|
||||||
|
Затронутые GET инвестиционных ресурсов возвращают сгенерированный contract
|
||||||
|
`{ data, meta }`. `data` nullable: `null` означает отсутствие complete result,
|
||||||
|
а подтверждённый пустой массив является available complete result. `meta`
|
||||||
|
независимо описывает freshness, completeness, `lastSuccessfulAt`,
|
||||||
|
resource-relevant `lastAttempt` и availability. `lastAttempt` имеет состояния
|
||||||
|
`none`, `running`, `succeeded`, `partial` и `failed`; его неуспех не заменяет
|
||||||
|
сохранённый complete result или его время. `Portfolio.isStale` остаётся
|
||||||
|
специфичным для snapshot полем, а не заменой состояния попытки.
|
||||||
|
|
||||||
|
Только Accounts дополнительно возвращает top-level `sync`: `null`, если у
|
||||||
|
текущего подключения ещё не было часовой попытки, иначе connection-wide
|
||||||
|
наблюдаемый результат. Это наблюдение, не команда. Обычный GET и retry после
|
||||||
|
проверки доступа читают локальное состояние и не начинают личную работу.
|
||||||
|
Устаревший `sync=observe`, пока он сохранён для совместимости маршрута, также
|
||||||
|
только читает. Request-started screen-sync command/status не является текущей
|
||||||
|
AS-IS моделью.
|
||||||
|
|
||||||
|
Безопасные ошибки завершённой часовой попытки передаются в `lastAttempt` и
|
||||||
|
Accounts `sync`; HTTP transport/access/input errors сохраняют Problem Details.
|
||||||
|
Старые данные при временной source failure сохраняют исходное успешное время.
|
||||||
|
Ни одно поле не раскрывает connection revision. Детали поведения —
|
||||||
|
[свежесть и полнота](cross-cutting/data-freshness.md).
|
||||||
|
|
||||||
|
Envelope несовместим с прежними bare-array ответами: backend/frontend
|
||||||
|
поставляются согласованной парой. Финальный artifact передаётся явно;
|
||||||
|
локальная генерация и CI frontend читают только собственный snapshot.
|
||||||
|
|
||||||
|
## Income analytics
|
||||||
|
|
||||||
|
`GET /api/v1/brokerage-accounts/{accountId}/income-analytics` и общий маршрут
|
||||||
|
`all` возвращают результат только выбранного периода: итоги по исходным
|
||||||
|
валютам, точные записи выплат, разбиение по инструментам и временные
|
||||||
|
интервалы. Данных сравнения с предыдущим периодом или годом ранее в ответе
|
||||||
|
нет — ни в теле, ни среди зависимостей `meta.dependencies`, роль которых
|
||||||
|
ограничена значением `selected`. Публичную машинную схему задаёт
|
||||||
|
backend-generated OpenAPI; frontend получает её обновлённый snapshot отдельно.
|
||||||
|
Точную семантику поведения описывает [доход](../domains/income/overview.md).
|
||||||
|
|
||||||
|
## Investment performance
|
||||||
|
|
||||||
|
`GET /api/v1/brokerage-accounts/{accountId}/performance` и общий маршрут
|
||||||
|
`all` возвращают [результат инвестирования](../domains/portfolio/investment-performance.md)
|
||||||
|
за всё время. Недоступность расчёта — не HTTP-ошибка: ответ `200` с
|
||||||
|
`data: null` и причинами в `meta.unavailable`. Неполная сводка «Все счета»
|
||||||
|
имеет `completeness: incomplete` и `excludedAccounts`. Доли передаются
|
||||||
|
десятичной строкой (`0.151` = 15,1 %), `return.basis` различает годовую
|
||||||
|
ставку и ставку за период. Схему задаёт backend-generated OpenAPI.
|
||||||
|
|
||||||
## Validation и identity
|
## Validation и identity
|
||||||
|
|
||||||
Backend преобразует и проверяет входные данные на общей HTTP-границе.
|
Backend преобразует и проверяет входные данные на общей HTTP-границе.
|
||||||
@@ -41,16 +103,11 @@ message/stack. Типичный ответ:
|
|||||||
|
|
||||||
Для input validation контракт требует `400`, `VALIDATION_ERROR` и `errors` как
|
Для input validation контракт требует `400`, `VALIDATION_ERROR` и `errors` как
|
||||||
массив `{ pointer, detail }`, где pointer адресует поле запроса. Это требование
|
массив `{ pointer, detail }`, где pointer адресует поле запроса. Это требование
|
||||||
не считается проверенным одним описанием OpenAPI: нужна проверка реального
|
проверяется реальным HTTP-ответом после всех стадий обработки запроса. Глобальная
|
||||||
HTTP-ответа после всех стадий обработки запроса.
|
валидация передаёт фильтру структурированные ошибки без исходных значений и DTO,
|
||||||
|
а фильтр формирует JSON Pointer, включая вложенные поля. HTTP suites подтверждают
|
||||||
**Известное расхождение:** согласованная
|
детали ошибок регистрации, банковской карты и синхронизации; OpenAPI отдельно
|
||||||
[спецификация аутентификации](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/user-authentication/spec.md#requirement-стандартные-ответы-об-ошибках)
|
фиксирует форму ответа.
|
||||||
требует `pointer` и `detail` для невалидных полей. В текущей конфигурации
|
|
||||||
валидация формирует строковые сообщения, а фильтр ожидает структурированные
|
|
||||||
ошибки полей; поэтому корректность деталей `errors` не подтверждена.
|
|
||||||
Исправление требует отдельного изменения реализации и проверки полного
|
|
||||||
HTTP-потока. OpenAPI сам по себе этого расхождения не устраняет.
|
|
||||||
|
|
||||||
Ответ не включает автоматически все поля RFC (`type`, `title`, `instance`).
|
Ответ не включает автоматически все поля RFC (`type`, `title`, `instance`).
|
||||||
Добавление обязательного поля является изменением публичного контракта.
|
Добавление обязательного поля является изменением публичного контракта.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,12 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ["@commitlint/config-conventional"],
|
extends: ["@commitlint/config-conventional"],
|
||||||
rules: { "type-enum": [2, "always", ["feat", "fix", "perf", "docs", "spec", "refactor", "build", "ci", "test", "style", "chore", "revert"]] },
|
defaultIgnores: false,
|
||||||
|
ignores: [
|
||||||
|
message => /^chore\(release\): \d+\.\d+\.\d+$/.test(message.trim()),
|
||||||
|
message => /^Merge pull request ['"].+['"] \(#\d+\) from .+ into .+$/.test(message.trim()),
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"type-enum": [2, "always", ["feat", "fix", "perf", "docs", "spec", "refactor", "build", "ci", "test", "style", "chore", "revert"]],
|
||||||
|
"references-empty": [2, "never"],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+30
-2
@@ -4,6 +4,34 @@
|
|||||||
типографику, интервалы, радиусы, тени и адаптивные размеры.
|
типографику, интервалы, радиусы, тени и адаптивные размеры.
|
||||||
Поддерживается светлая схема; тёмная тема не заявлена.
|
Поддерживается светлая схема; тёмная тема не заявлена.
|
||||||
|
|
||||||
|
## Общая основа и каталог
|
||||||
|
|
||||||
|
Внутри frontend используются три private npm workspace-пакета:
|
||||||
|
|
||||||
|
| Пакет | Ответственность |
|
||||||
|
| --- | --- |
|
||||||
|
| `@wealthledger/tokens` | JSON со значениями палитры, типографики, интервалов и других визуальных параметров |
|
||||||
|
| `@wealthledger/ui` | Mantine-тема, общие стили, StateView, LoadingSkeleton и EntryCard |
|
||||||
|
| `@wealthledger/icons` | React-иконки добавления, редактирования и удаления |
|
||||||
|
|
||||||
|
EntryCard отвечает за расположение переданных подписей, значения и действий.
|
||||||
|
Предметный компонент определяет смысл операции, форматирует данные и выбирает
|
||||||
|
цветовую роль. Денежные расчёты и запросы не входят в общий UI-пакет.
|
||||||
|
|
||||||
|
Иконки наследуют цвет через `currentColor`. По умолчанию они декоративные:
|
||||||
|
доступное имя принадлежит кнопке. Самостоятельная смысловая иконка получает
|
||||||
|
явные роль изображения и доступное имя. Предметные рисунки и анимации
|
||||||
|
остаются в приложении; поставки raw SVG и мобильного UI нет.
|
||||||
|
|
||||||
|
Storybook объединяет примеры приложения и пакетов. Он показывает синтетические
|
||||||
|
состояния без API, авторизации и query providers; формы получают локальные
|
||||||
|
callbacks. Каталог помогает проверять представление, но не заменяет проверки
|
||||||
|
запросов, денежных ограничений и восстановления после ошибок.
|
||||||
|
|
||||||
|
Пакеты используются локально и не опубликованы в registry. Независимый выпуск
|
||||||
|
не настроен. Потребители импортируют публичные exports пакетов напрямую; прежние
|
||||||
|
compatibility re-exports `shared/theme` и `shared/ui` удалены.
|
||||||
|
|
||||||
## Семантические роли
|
## Семантические роли
|
||||||
|
|
||||||
| Роль | Цветовая основа | Применение |
|
| Роль | Цветовая основа | Применение |
|
||||||
@@ -16,8 +44,8 @@
|
|||||||
| border | gray.3 | Граница поверхности |
|
| border | gray.3 | Граница поверхности |
|
||||||
| primary | blue filled | Основное действие и активная навигация |
|
| primary | blue filled | Основное действие и активная навигация |
|
||||||
| info | blue.6 | Информация |
|
| info | blue.6 | Информация |
|
||||||
| positive | green.6 | Положительный результат с текстом или знаком |
|
| positive | green.9 | Положительный результат с текстом или знаком |
|
||||||
| negative | red.6 | Ошибка или отрицательный результат с текстом или знаком |
|
| negative | red.9 | Ошибка или отрицательный результат с текстом или знаком |
|
||||||
| warning | yellow.6 | Предупреждение; не мелкий текст на белом |
|
| warning | yellow.6 | Предупреждение; не мелкий текст на белом |
|
||||||
|
|
||||||
Цвет не заменяет подпись или знак. Палитра не гарантирует доступный контраст
|
Цвет не заменяет подпись или знак. Палитра не гарантирует доступный контраст
|
||||||
|
|||||||
@@ -7,30 +7,11 @@
|
|||||||
только к backend. Счёт не хранится как долговременное локальное владение
|
только к backend. Счёт не хранится как долговременное локальное владение
|
||||||
независимо от брокерского подключения.
|
независимо от брокерского подключения.
|
||||||
|
|
||||||
```mermaid
|
Экран читает локальный список и сохранённые сводки. Обычный запрос запускает
|
||||||
sequenceDiagram
|
недостающие или устаревшие зависимости в фоне. Список и сводка каждого счёта
|
||||||
participant U as Пользователь
|
имеют независимые timestamps и состояния. Неподтверждённый список показывает
|
||||||
participant F as Frontend
|
загрузку доступа, а не отсутствие счетов. Поток и границы описаны в
|
||||||
participant B as Backend
|
[модели свежести](../../architecture/cross-cutting/data-freshness.md).
|
||||||
participant D as SQLite
|
|
||||||
participant T as T-Bank gRPC
|
|
||||||
U->>F: Открывает /accounts
|
|
||||||
F->>B: GET /api/v1/brokerage-accounts (Bearer)
|
|
||||||
B->>D: Читает подключение T-Bank
|
|
||||||
alt Подключение отсутствует
|
|
||||||
B-->>F: 409 TBANK_NOT_CONNECTED
|
|
||||||
F-->>U: Переход к настройкам T-Bank
|
|
||||||
else Подключение сохранено
|
|
||||||
B->>T: GetAccounts(OPEN) over TLS
|
|
||||||
T-->>B: Счета или ошибка
|
|
||||||
par Для каждого поддерживаемого счета
|
|
||||||
B->>T: GetPortfolio + GetPositions over TLS
|
|
||||||
T-->>B: Позиции и деньги либо временная ошибка
|
|
||||||
end
|
|
||||||
B-->>F: Счета со сводками либо ошибка
|
|
||||||
F-->>U: Карточки счетов или объяснение ошибки
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Счёт и его сводка
|
## Счёт и его сводка
|
||||||
|
|
||||||
@@ -69,8 +50,8 @@ sequenceDiagram
|
|||||||
сводка не подменяется нулями.
|
сводка не подменяется нулями.
|
||||||
|
|
||||||
Временная недоступность сводки одного счёта остаётся локальной для него.
|
Временная недоступность сводки одного счёта остаётся локальной для него.
|
||||||
Ошибка подключения, отклонение токена или невозможность получить исходный
|
Известный отказ доступа скрывает личные данные. Transient failure сохраняет
|
||||||
список не превращаются в частичный успешный список.
|
последний список и его время; отсутствие первого списка остаётся missing.
|
||||||
|
|
||||||
## Состояния
|
## Состояния
|
||||||
|
|
||||||
@@ -81,20 +62,22 @@ sequenceDiagram
|
|||||||
|
|
||||||
- `409 TBANK_NOT_CONNECTED`: список не запрашивается у T-Bank, экран ведёт в настройки.
|
- `409 TBANK_NOT_CONNECTED`: список не запрашивается у T-Bank, экран ведёт в настройки.
|
||||||
- `400 INVALID_TBANK_TOKEN`: сохранённый токен не удаляется, экран ведёт в настройки для замены или удаления.
|
- `400 INVALID_TBANK_TOKEN`: сохранённый токен не удаляется, экран ведёт в настройки для замены или удаления.
|
||||||
- `503 TBANK_UNAVAILABLE`: экран предлагает повторить загрузку или открыть настройки.
|
- `503 TBANK_UNAVAILABLE`: экран показывает спокойное состояние недоступности; чтение возобновляется автоматически.
|
||||||
- Неизвестная ошибка: экран предлагает только повторить загрузку.
|
- Неизвестная ошибка: то же состояние недоступности без действия повтора.
|
||||||
- Успешный пустой ответ: штатное состояние отсутствия инвестиционных счетов.
|
- Успешный пустой ответ: штатное состояние отсутствия инвестиционных счетов.
|
||||||
|
|
||||||
Карточка ведёт на `/accounts/:accountId` по всей области и доступна с клавиатуры. На широком экране карточки расположены в две колонки, на узком — в одну. Карточка показывает название, локализованный тип, крупный валютный итог и вторую валюту при наличии, число остальных валют, дату открытия без времени и видимое действие открытия. Отдельные цветные блоки «Акции» и «Облигации» показывают количество наименований бумаг. Недоступная или пустая сводка отображается локально; отсутствие сумм не скрывает известные количества бумаг, а недоступная сводка не подменяет их нулями. Наведение и видимый клавиатурный фокус подчёркивают переход; настройка уменьшения движения отключает анимацию.
|
Карточка ведёт на `/accounts/:accountId` по всей области и доступна с клавиатуры. Карточки расположены в одну колонку при любой ширине. Карточка показывает название, локализованный тип, крупный валютный итог и вторую валюту при наличии, число остальных валют, дату открытия без времени и видимое действие открытия. Отдельные цветные блоки «Акции» и «Облигации» показывают количество наименований бумаг. Недоступная или пустая сводка отображается локально; отсутствие сумм не скрывает известные количества бумаг, а недоступная сводка не подменяет их нулями. Наведение и видимый клавиатурный фокус подчёркивают переход; настройка уменьшения движения отключает анимацию.
|
||||||
|
|
||||||
## Свежесть и границы
|
## Свежесть и границы
|
||||||
|
|
||||||
Список запрашивается при открытии экрана без ручного обновления и без обновления при возврате фокуса. Frontend использует успешный кэш TanStack Query в течение 60 секунд, поэтому повторный вход в этот интервал не выполняет новый GET. Когда уже показанный список обновляется в фоне, он остаётся на экране: индикатор обновления и ошибка с повторной попыткой отображаются рядом с последним успешным ответом, включая пустой список. Это клиентский сохранённый ответ; серверный запасной список не выдаётся. Backend не хранит кэш и не передаёт метку свежести. Список не заменяет полный [портфель](../portfolio/overview.md),
|
Обычное открытие использует query owner и локальный server snapshot.
|
||||||
[журнал операций](../operations/overview.md) или историю закрытых счетов.
|
«Обновить» перечитывает сохранённые membership и сводки доступных счетов;
|
||||||
Для совокупного результата доступен [режим «Все счета»](../portfolio/all-accounts.md).
|
активная работа наблюдается без нового импорта. Истечение TTL само не
|
||||||
|
запускает запрос: обновлением личных данных владеет часовой обход.
|
||||||
|
Membership и portfolio source имеют часовую свежесть. Время списка
|
||||||
|
не подменяет время каждой сводки. Список не заменяет полный
|
||||||
|
[портфель](../portfolio/overview.md), [журнал](../operations/overview.md)
|
||||||
|
или историю закрытых счетов. Для общего результата доступен
|
||||||
|
[режим «Все счета»](../portfolio/all-accounts.md).
|
||||||
|
|
||||||
Согласованное поведение описывает [спецификация счетов](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-accounts/spec.md).
|
Согласованное поведение описывает [спецификация счетов](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-accounts/spec.md).
|
||||||
Её раздел об экране пока требует одноколоночного списка и не описывает
|
|
||||||
счётчики акций и облигаций. Текущая реализация и её проверки используют
|
|
||||||
две колонки на широком экране и показывают эти счётчики. Расхождение требует
|
|
||||||
отдельного согласования требований; в этой документации отражён наблюдаемый экран.
|
|
||||||
|
|||||||
@@ -11,14 +11,20 @@
|
|||||||
|
|
||||||
Пользователь регистрируется или входит по email и паролю. Регистрация сразу
|
Пользователь регистрируется или входит по email и паролю. Регистрация сразу
|
||||||
создаёт первую сессию. Внешние пробелы email удаляются, буквы приводятся
|
создаёт первую сессию. Внешние пробелы email удаляются, буквы приводятся
|
||||||
к нижнему регистру. Backend требует пароль не короче 12 символов и не длиннее
|
к нижнему регистру.
|
||||||
72 UTF-8 байтов.
|
|
||||||
|
|
||||||
**Известное расхождение:** сохранённое требование регистрации и проверка
|
Пароль при регистрации — от 8 до 72 печатных ASCII-символов: латинские
|
||||||
в форме frontend считают нижнюю границу в UTF-8 байтах, а backend — в символах.
|
буквы, цифры, спецсимволы и пробел. Кириллица, emoji, другие не-ASCII и
|
||||||
Не-ASCII пароль может пройти форму и получить отказ API. Это не принятое
|
управляющие символы отклоняются. Пароль не обрезается и не нормализуется, а
|
||||||
изменение password policy; требуется отдельное согласованное решение
|
обязательных классов символов нет. Верхняя граница вытекает из bcrypt, который
|
||||||
о проверке пароля и подтверждение полного HTTP-потока.
|
учитывает только первые 72 байта: для ASCII символ равен байту, поэтому
|
||||||
|
длинный пароль не обрезается молча. Frontend и backend проверяют одну и ту же
|
||||||
|
политику.
|
||||||
|
|
||||||
|
При входе проверяется только то, что пароль непустой и не длиннее 72 UTF-8
|
||||||
|
байтов. Минимум и набор символов не применяются, чтобы пользователь,
|
||||||
|
зарегистрированный по прежним правилам (в том числе с не-ASCII паролем), мог
|
||||||
|
войти. Невалидный пароль получает `400` с кодом `VALIDATION_ERROR`.
|
||||||
|
|
||||||
Access token действует 15 минут и хранится только в памяти браузера.
|
Access token действует 15 минут и хранится только в памяти браузера.
|
||||||
Refresh token имеет серверный срок 7 дней, заменяется при восстановлении
|
Refresh token имеет серверный срок 7 дней, заменяется при восстановлении
|
||||||
@@ -55,15 +61,16 @@ sequenceDiagram
|
|||||||
B-->>F: Access token, профиль, новая refresh-cookie
|
B-->>F: Access token, профиль, новая refresh-cookie
|
||||||
F->>B: Protected request с Bearer
|
F->>B: Protected request с Bearer
|
||||||
else Cookie нет или refresh отклонён
|
else Cookie нет или refresh отклонён
|
||||||
B-->>F: 401 и очистка cookie
|
B-->>F: 401 без изменения cookie
|
||||||
Note over F: Готовое неавторизованное состояние
|
Note over F: Готовое неавторизованное состояние
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
В одном экземпляре frontend одновременные refresh используют один
|
В одной вкладке одновременные refresh используют один незавершённый запрос;
|
||||||
незавершённый запрос. Между вкладками используется Web Locks, если браузер
|
cookie-writing requests упорядочены локально. Межвкладочная синхронизация
|
||||||
его поддерживает. Backend всё равно защищает ротацию атомарно: координация
|
не поддерживается: после смены сессии в другой вкладке допустимы reload/re-login.
|
||||||
браузеров не заменяет серверную проверку.
|
Backend защищает ротацию атомарно. Неуспешный refresh не удаляет cookie
|
||||||
|
победившего запроса; недействительный token по-прежнему не восстанавливает сессию.
|
||||||
|
|
||||||
Обычный защищённый запрос с `401 UNAUTHORIZED` проходит refresh и один повтор
|
Обычный защищённый запрос с `401 UNAUTHORIZED` проходит refresh и один повтор
|
||||||
через общий транспорт; auth endpoints из этого цикла исключены. Само
|
через общий транспорт; auth endpoints из этого цикла исключены. Само
|
||||||
|
|||||||
@@ -89,9 +89,8 @@ JavaScript Number. Отдельный ввод ограничен signed64: на
|
|||||||
следующие страницы. Баланс при выборе прошлого периода остаётся текущим.
|
следующие страницы. Баланс при выборе прошлого периода остаётся текущим.
|
||||||
Сводка учитывает все подходящие записи, включая не показанные на странице.
|
Сводка учитывает все подходящие записи, включая не показанные на странице.
|
||||||
|
|
||||||
Сводка одной или всех карт показывает одну итоговую сумму расходов с учётом
|
Сводка одной или всех карт отдельно показывает расходы, возвращённую сумму и
|
||||||
возвратов и компактные строки категорий по убыванию итоговых расходов. Отдельные показатели «Возвращено»
|
итог после возвратов, а также компактные строки категорий по убыванию итоговых расходов. Возврат уменьшает расходы месяца своего
|
||||||
и «После возвратов» в интерфейсе скрыты; расчёт и операции возврата сохранены. Возврат уменьшает расходы месяца своего
|
|
||||||
получения: августовская покупка 1 000 ₽ и сентябрьский возврат 600 ₽ дают
|
получения: августовская покупка 1 000 ₽ и сентябрьский возврат 600 ₽ дают
|
||||||
августовский итог 1 000 ₽ и сентябрьский −600 ₽ при отсутствии других трат.
|
августовский итог 1 000 ₽ и сентябрьский −600 ₽ при отсутствии других трат.
|
||||||
Начальный остаток, пополнения и переводы в эти показатели не входят.
|
Начальный остаток, пополнения и переводы в эти показатели не входят.
|
||||||
|
|||||||
+32
-40
@@ -4,7 +4,7 @@
|
|||||||
купонами и дивидендами за выбранный период». Пользователь открывает
|
купонами и дивидендами за выбранный период». Пользователь открывает
|
||||||
`/accounts/:accountId/income` из обзора счёта или
|
`/accounts/:accountId/income` из обзора счёта или
|
||||||
[общий доход](../portfolio/all-accounts.md) по доступным счетам.
|
[общий доход](../portfolio/all-accounts.md) по доступным счетам.
|
||||||
Экран показывает итоги по валютам, сравнение и отдельную карточку каждой
|
Экран показывает итоги по валютам и отдельную карточку каждой
|
||||||
включённой выплаты. Источником служат [исполненные операции](../operations/overview.md),
|
включённой выплаты. Источником служат [исполненные операции](../operations/overview.md),
|
||||||
а не изменение цены портфеля и не календарь будущих выплат.
|
а не изменение цены портфеля и не календарь будущих выплат.
|
||||||
|
|
||||||
@@ -38,44 +38,36 @@
|
|||||||
|
|
||||||
При первой загрузке или новом периоде без сохранённых данных показан
|
При первой загрузке или новом периоде без сохранённых данных показан
|
||||||
каркас страницы. Пустой результат объясняет отсутствие исполненных купонов и
|
каркас страницы. Пустой результат объясняет отсутствие исполненных купонов и
|
||||||
дивидендов, сохраняя фильтры, налоговое пояснение и сравнения. Ошибка запроса
|
дивидендов, сохраняя фильтры и налоговое пояснение. Ошибка запроса
|
||||||
или нарушение принадлежности выплат даёт локальное состояние с действием «Повторить».
|
или нарушение принадлежности выплат даёт локальное состояние недоступности
|
||||||
|
без действия повтора; чтение восстанавливается автоматически.
|
||||||
При фоновом обновлении того же периода сохранённый ответ и черновик
|
При фоновом обновлении того же периода сохранённый ответ и черновик
|
||||||
остаются видимыми: загрузку обозначает индикатор, неуспешное обновление —
|
остаются видимыми без индикации обновления и технического предупреждения.
|
||||||
предупреждение с повторной попыткой.
|
Неполнота применённого периода обозначается состоянием самого результата.
|
||||||
|
|
||||||
На узком экране поля и действия идут вертикально; все карточки выплат
|
На узком экране поля и действия идут вертикально; все карточки выплат
|
||||||
остаются в общем потоке страницы без раскрытия или внутренних областей прокрутки.
|
остаются в общем потоке страницы без раскрытия или внутренних областей прокрутки.
|
||||||
|
|
||||||
## Какие выплаты входят в результат
|
## Какие выплаты входят в результат
|
||||||
|
|
||||||
- Backend проверяет доступ к счёту, проходит все страницы исполненных операций и не возвращает частичный результат при ошибке страницы или повреждённой цепочке курсоров.
|
- Backend проверяет доступ локально и читает сохранённые операции. Фоновая загрузка должна завершить все страницы; ошибка страницы или повреждённая цепочка курсоров не подтверждают полноту.
|
||||||
- Только точные типы купона и дивиденда источника формируют результат. Суммы остаются точными десятичными строками в исходной валюте; отрицательные корректировки сохраняют знак. Налоги, комиссии, сделки, переводы, погашения и переводы дивидендов не входят в доход.
|
- Только точные типы купона и дивиденда источника формируют результат. Суммы остаются точными десятичными строками в исходной валюте; отрицательные корректировки сохраняют знак. Налоги, комиссии, сделки, переводы, погашения и переводы дивидендов не входят в доход.
|
||||||
- Итоги, сравнения, временные интервалы и разбиение по инструментам не смешивают валюты и не создают общий конвертированный итог. На странице показано только сравнение с годом ранее, когда календарная дата и история счёта позволяют. Предыдущий период остаётся в ответе API, но не отображается.
|
- Итоги, временные интервалы и разбиение по инструментам не смешивают валюты и не создают общий конвертированный итог. Система не загружает другие периоды ради сравнения полученного дохода с выбранным.
|
||||||
- Временные интервалы по-прежнему приходят с backend: дни для 1–45 дней, недели для 46–180 и месяцы для более длинного периода, включая пустые интервалы. Страница дохода не строит из них график или текстовую динамику; ответ API не меняется.
|
- Временные интервалы по-прежнему приходят с backend: дни для 1–45 дней, недели для 46–180 и месяцы для более длинного периода, включая пустые интервалы. Страница дохода не строит из них график или текстовую динамику; ответ API не меняется.
|
||||||
- Раздел «Выплаты» показывает единый список карточек выплат в исходном порядке `entries`: одна карточка на каждую выплату. Внешних групп по инструментам или валютам, итогов и количества выплат инструмента нет. Описание, дата, доступные идентификаторы инструмента и сумма со знаком видны в каждой карточке; валюты по-прежнему разделены в итогах и сравнениях. Дополнительный запрос за составом не нужен.
|
- Раздел «Выплаты» показывает карточки выплат в исходном порядке `entries`. В отдельном счёте каждая выплата — отдельная карточка. В режиме «Все счета» выплаты одного инструмента, дня и точного типа образуют группу, если их две и более ([правила группировки](../operations/overview.md#группировка-в-режиме-все-счета)). Группа стоит на месте первой выплаты; каждая строка счёта показывает название счёта и свою сумму со знаком. Визуальный итог группы — точная сумма по валютам по показанным выплатам; он скрыт, если полнота периода не `complete`, или у выплаты нет суммы, или сумма равна нулю. Это клиентский расчёт и не заменяет серверные итоги аналитики дохода, которые по-прежнему приходят из ответа API и не пересчитываются. Описание, дата, доступные идентификаторы инструмента и сумма со знаком видны в каждой выплате. Дополнительный запрос за составом не нужен.
|
||||||
|
|
||||||
Сравнение с непосредственно предыдущим периодом сохраняется в API:
|
|
||||||
для полного месяца это предыдущий полный месяц, иначе — непосредственно
|
|
||||||
предшествующий интервал той же включительной длительности. Текущая страница
|
|
||||||
его не показывает. Это расходится с
|
|
||||||
[основным требованием сравнений](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-income-analytics/spec.md#requirement-period-comparisons-preserve-calendar-and-currency-meaning),
|
|
||||||
которое описывает оба сравнения. Требуется отдельно согласовать этот разрыв;
|
|
||||||
здесь сохранено различие между ответом API и фактическим экраном.
|
|
||||||
|
|
||||||
Раздел «Годом ранее» показывает цветные панели без общей белой подложки и
|
|
||||||
внутренних отступов у внешнего блока. Точные периоды показаны в заголовках двух панелей с русским форматом диапазона
|
|
||||||
и UTC для календарных дат.
|
|
||||||
Отдельного заголовка с датами и валютного бейджа нет; валюта остаётся у каждой суммы.
|
|
||||||
Панель «Разница» сохраняет серверную абсолютную разницу и дополняет итог, купоны
|
|
||||||
и дивиденды процентом: абсолютная разница / сумма годом ранее × 100.
|
|
||||||
Это округлённый показатель для отображения, а не пересчёт Money или инвестиционная
|
|
||||||
доходность. Форматирование сохраняет знак и не более одного десятичного разряда.
|
|
||||||
При нулевой или отрицательной базе либо непредставимом числовом отношении
|
|
||||||
процентный бейдж не отображается; абсолютная разница остаётся видимой.
|
|
||||||
|
|
||||||
## Полнота и принадлежность выплат
|
## Полнота и принадлежность выплат
|
||||||
|
|
||||||
|
Доходы читают сохранённые операции и coverage в согласованной транзакции.
|
||||||
|
Только выбранный период определяет готовность результата. Неполный период
|
||||||
|
не получает окончательных итогов.
|
||||||
|
Подтверждённый пустой период отличается от ещё не загруженного. При смене
|
||||||
|
периода прежний snapshot не показывается под новым названием.
|
||||||
|
|
||||||
|
Ручная сверка полного периода сохраняет весь прежний результат до успешной
|
||||||
|
атомарной публикации всех необходимых интервалов и счетов. История счёта
|
||||||
|
может ещё догружаться после готовности выбранного месяца.
|
||||||
|
|
||||||
Каждая запись `income-analytics.entries` содержит обязательный `instrumentKey`,
|
Каждая запись `income-analytics.entries` содержит обязательный `instrumentKey`,
|
||||||
равный `key` соответствующей строки `instruments`. Backend вычисляет этот ключ
|
равный `key` соответствующей строки `instruments`. Backend вычисляет этот ключ
|
||||||
один раз при подготовке записей дохода и использует его же для группировки.
|
один раз при подготовке записей дохода и использует его же для группировки.
|
||||||
@@ -83,26 +75,26 @@
|
|||||||
серверным правилом; клиент воспринимает ключ как непрозрачную строку.
|
серверным правилом; клиент воспринимает ключ как непрозрачную строку.
|
||||||
Это поле принадлежит ответу аналитики; общий журнал операций его не требует.
|
Это поле принадлежит ответу аналитики; общий журнал операций его не требует.
|
||||||
|
|
||||||
Frontend проверяет принадлежность выплат по полученному ключу до подготовки
|
Backend обеспечивает принадлежность выплат, валюту и receipt count при
|
||||||
плоского списка выплат. Отсутствующий, пустой или
|
построении entries и instrument groups из одного набора данных. Frontend
|
||||||
нестроковый ключ, неоднозначные ключи инструментов, неизвестная группа, другая
|
доверяет типизированному contract, не восстанавливает формулу идентификации
|
||||||
валюта или несовпадение числа выплат приводят к существующему локальному
|
и не выполняет второй consistency validator. Полностью пустой ответ допустим.
|
||||||
состоянию ошибки аналитики с повторной попыткой. Частичный список выплат не показывается как
|
Состав и исходный серверный порядок записей и знак сумм
|
||||||
полный; формула идентификации в клиенте не восстанавливается. Полностью пустой
|
сохраняются; серверные денежные итоги frontend не пересчитывает (визуальный итог группы в режиме «Все счета» отдельный, см. выше). Стабильный ID в пределах подключения и счёта определяет одну операцию:
|
||||||
ответ допустим. Состав и исходный серверный порядок записей и знак сумм
|
повторная сверка обновляет её без двойного учёта. Одинаковые ID разных счетов
|
||||||
сохраняются, frontend не пересчитывает денежные итоги. Повторяющиеся ID
|
остаются разными выплатами. Изменение источника логотипа позволяет заново
|
||||||
операций остаются отдельными строками; повторные вхождения учитываются раздельно, а изменение источника логотипа
|
показать изображение после буквенной заглушки.
|
||||||
позволяет заново показать изображение после буквенной заглушки.
|
|
||||||
|
|
||||||
## Будущие события и ограничения
|
## Будущие события и ограничения
|
||||||
|
|
||||||
На странице дохода нет блоков «Динамика» и «Ближайший прогноз». Она не запрашивает
|
На странице дохода нет блоков «Динамика» и «Ближайший прогноз», блоков
|
||||||
|
сравнения периодов и сообщений об их недоступности. Она не запрашивает
|
||||||
`/events` и не зависит от его доступности. Самостоятельная страница
|
`/events` и не зависит от его доступности. Самостоятельная страница
|
||||||
[«Будущие события»](upcoming-events.md) по-прежнему доступна из обзора
|
[«Будущие события»](upcoming-events.md) по-прежнему доступна из обзора
|
||||||
выбранного счёта и сохраняет прежнее поведение. Будущие выплаты
|
выбранного счёта и сохраняет прежнее поведение. Будущие выплаты
|
||||||
не включаются в итоги полученного дохода или сравнения.
|
не включаются в итоги полученного дохода.
|
||||||
|
|
||||||
Аналитика только читает данные, не хранит историю операций и не рассчитывает налоги, инвестиционную эффективность, доходность, валютную конвертацию или общий мультивалютный итог.
|
Аналитика читает сохранённую историю операций и не рассчитывает налоги, доходность (её показывает [результат инвестирования](../portfolio/investment-performance.md)), валютную конвертацию или общий мультивалютный итог.
|
||||||
|
|
||||||
Основное проверяемое поведение — в [спецификации дохода](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-income-analytics/spec.md),
|
Основное проверяемое поведение — в [спецификации дохода](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-income-analytics/spec.md),
|
||||||
точная форма ответа — в [HTTP API](../../architecture/http-api.md).
|
точная форма ответа — в [HTTP API](../../architecture/http-api.md).
|
||||||
|
|||||||
@@ -39,66 +39,76 @@
|
|||||||
|
|
||||||
## Период и список
|
## Период и список
|
||||||
|
|
||||||
Без фильтра период начинается сегодня в UTC и заканчивается через 30 дней
|
Без фильтра страница применяет период по умолчанию из семи календарных
|
||||||
после начала, обе даты включены. Поля «С» и «По» позволяют изменить период:
|
дат включительно: сегодняшней и шести следующих календарных дней. API при
|
||||||
«Применить» сохраняет выбранные календарные даты в URL и запрашивает результат,
|
отсутствии параметров использует своё окно (от текущей даты UTC до `from + 30`
|
||||||
«Сбросить» возвращает период по умолчанию. Обзор счёта сохраняет прогноз
|
календарных дней), обе даты включены; `from` и `to` принимают только строгие
|
||||||
на текущую дату и следующие 30 дней.
|
даты `YYYY-MM-DD`. Календарь фильтра разрешает выбор сегодняшней и будущих дат
|
||||||
|
и не позволяет выбрать даты раньше сегодняшней. Прямая ссылка с валидным
|
||||||
|
прошедшим периодом сохраняет указанные даты без замены; некорректный формат,
|
||||||
|
несуществующая календарная дата или обратный диапазон нормализуются к периоду
|
||||||
|
по умолчанию с удалением параметров из URL. Поля «С» и «По» сохраняют
|
||||||
|
выбранные календарные даты в URL, а «Сбросить» возвращает период по умолчанию.
|
||||||
|
|
||||||
События упорядочены по календарной дате. У каждой строки видны тип,
|
События упорядочены по календарной дате. Купоны и дивиденды показываются
|
||||||
|
единым предметным представлением выплаты с явной пометкой прогноза — тем же,
|
||||||
|
что у исполненных выплат в доходе и журнале; неизвестная сумма остаётся
|
||||||
|
«сумма неизвестна» и не превращается в ноль или полученные деньги.
|
||||||
|
Дата дивиденда обозначена как дата реестра. Offer и погашение сохраняют своё
|
||||||
|
значение без суммы выплаты. У каждой строки видны тип,
|
||||||
инструмент, дата и доступная сумма в исходной валюте. Список не имеет
|
инструмент, дата и доступная сумма в исходной валюте. Список не имеет
|
||||||
пагинации и не конвертирует валюты. Пустой результат означает отсутствие
|
пагинации и не конвертирует валюты. Пустой результат означает отсутствие
|
||||||
известных поддерживаемых событий в выбранном окне, а не доказательство
|
известных поддерживаемых событий в выбранном окне, а не доказательство
|
||||||
отсутствия всех будущих выплат. Инструмент без поддерживаемых данных
|
отсутствия всех будущих выплат. Инструмент без поддерживаемых данных
|
||||||
не получает искусственную строку «нет данных».
|
не получает искусственную строку «нет данных».
|
||||||
|
|
||||||
API принимает необязательные `from` и `to` в формате `YYYY-MM-DD` для
|
Сохранённый forecast сохраняет свой `calculatedAt` и исходное включительное
|
||||||
отдельного и общего счёта. Без `from` используется сегодня в UTC,
|
окно. GET для отдельного счёта или `all` возвращает только пересечение этого
|
||||||
без `to` — 30 дней после начала. Неверный формат или несуществующая дата
|
окна и запрошенного UTC-периода, не меняя ни `calculatedAt`, ни исходное окно.
|
||||||
дают `400`; обратный период — `400 INVALID_EVENTS_PERIOD`.
|
Пустое пересечение или отсутствие событий в пересечении — успешный пустой
|
||||||
|
список с теми же metadata; это не отсутствие forecast. Неверный формат или
|
||||||
|
несуществующая дата дают `400`; обратный период — `400 INVALID_EVENTS_PERIOD`
|
||||||
|
без замены периода и без запуска личной работы.
|
||||||
Фильтрация на сервере предшествует сортировке и вычислению
|
Фильтрация на сервере предшествует сортировке и вычислению
|
||||||
`incomeTotalsByCurrency`, включающего только купоны и дивиденды.
|
`incomeTotalsByCurrency`, включающего только купоны и дивиденды.
|
||||||
Итоги остаются в ответе API; полный экран показывает список без общего итога.
|
Итоги остаются в ответе API; полный экран показывает список без общего итога списка.
|
||||||
|
|
||||||
Текущие controller, сервис и экран поддерживают выбор периода, тогда как
|
В режиме «Все счета» события одного инструмента, даты, точного типа и `dateKind` образуют
|
||||||
[основная спецификация](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-upcoming-events/spec.md#requirement-account-page-presents-an-upcoming-events-list)
|
группу, если их две и более, в том числе одного счёта. Ключ дополнительно включает `dateKind`,
|
||||||
описывает фиксированные 30 дней и экран без фильтров. Документация отражает
|
поэтому дата реестра дивиденда и иная дата того же инструмента не объединяются. Группа стоит на месте первого
|
||||||
реализованный выбор периода, не изменяя это несогласованное требование.
|
события. Полный список группирует все полученные события; превью обзора сначала берёт пять
|
||||||
|
событий и группирует только их. Каждая строка счёта остаётся отдельной и показывает название
|
||||||
|
счёта и свою прогнозную сумму. Визуальный итог группы — точная сумма по валютам; он показывается
|
||||||
|
только если полнота прогноза `complete` и у каждого события группы известна прогнозная сумма.
|
||||||
|
Иначе итог скрыт, а строки остаются. Этот итог не равен итогам ответа API и не относится к полученному доходу.
|
||||||
|
|
||||||
## Получение и отказы
|
## Получение и отказы
|
||||||
|
|
||||||
```mermaid
|
Запрос требует сессии, подключения и локально подтверждённого membership.
|
||||||
sequenceDiagram
|
Он читает сохранённый forecast и не запускает личную синхронизацию. Часовой
|
||||||
participant F as Frontend
|
обход строит forecast из полного личного portfolio и общих календарей: T-Bank
|
||||||
participant B as Backend
|
для дивидендов, MOEX для bond events. Личные оценки сохраняются отдельно от
|
||||||
participant T as T-Bank
|
общих календарей.
|
||||||
participant M as MOEX ISS
|
|
||||||
F->>B: Счёт и выбранный период событий
|
|
||||||
B->>B: Проверяет сессию и текущее подключение
|
|
||||||
B->>T: Получает открытые счета и позиции доступного счёта
|
|
||||||
par События акций
|
|
||||||
B->>T: GetDividends за выбранный период
|
|
||||||
T-->>B: Известные дивиденды
|
|
||||||
and События облигаций
|
|
||||||
B->>M: Календарь bondization
|
|
||||||
M-->>B: Купоны, погашения и оферты
|
|
||||||
end
|
|
||||||
B->>B: Применяет период и текущее количество бумаг
|
|
||||||
B-->>F: Полный список известных событий либо ошибка
|
|
||||||
```
|
|
||||||
|
|
||||||
Запрос требует действующей сессии и подключения T-Bank. Получение
|
Последний полный forecast привязан к scope, исходному окну и membership. Если
|
||||||
необходимых финансовых данных брокера или календаря MOEX завершается ошибкой
|
часовой обход не может получить полный новый forecast, прежний полный прогноз,
|
||||||
`503`, если источник недоступен: частичный список не выдаётся как полный.
|
его `calculatedAt` и исходное окно остаются читаемыми, в том числе после
|
||||||
Доступные метаданные инструментов дополняют название и логотип; когда
|
рестарта; `meta.lastAttempt` отдельно сообщает об ошибке обновления. Без
|
||||||
исходного тикера достаточно для календаря, ошибка дополнительного поиска
|
полного forecast `data: null` и metadata обозначает initial loading либо
|
||||||
метаданных не скрывает события.
|
временную недоступность, а не подтверждённое отсутствие выплат. В режиме `all`
|
||||||
|
результат complete только при полном вкладе каждого включённого счёта. Другой
|
||||||
|
scope или изменившийся membership не получают старый forecast под новым
|
||||||
|
названием.
|
||||||
|
Успешный ответ MOEX без запрошенного блока означает отсутствие поддерживаемых
|
||||||
|
событий этого источника; это отличается от transport failure и повреждённого
|
||||||
|
формата ответа.
|
||||||
|
|
||||||
При первом открытии показан каркас заголовка и строк, без интерактивных
|
При первом открытии без complete forecast показан каркас заголовка и строк,
|
||||||
заглушек. Пустой ответ сохраняет возможность изменить период.
|
без интерактивных заглушек. Пустой успешный ответ сохраняет возможность
|
||||||
При обычном фоновом обновлении уже полученные данные остаются на экране,
|
изменить период. При failed HTTP reread уже полученный Client Cached Snapshot
|
||||||
ошибка получает предупреждение и повторную попытку. Без выбранного счёта
|
остаётся на экране; это отличается от server-side failed hourly attempt.
|
||||||
запрос не выполняется. Смена сессии и подключения следует
|
Повторная попытка перечитывает только HTTP-ответ. Без выбранного счёта запрос
|
||||||
|
не выполняется. Смена сессии и подключения следует
|
||||||
[общим правилам финансовых данных](../../architecture/cross-cutting/data-freshness.md).
|
[общим правилам финансовых данных](../../architecture/cross-cutting/data-freshness.md).
|
||||||
|
|
||||||
Внешние границы объясняют интеграции [T-Bank](../../integrations/tbank.md)
|
Внешние границы объясняют интеграции [T-Bank](../../integrations/tbank.md)
|
||||||
|
|||||||
@@ -89,7 +89,11 @@ HTML-источник, общий кэш и поведение при измен
|
|||||||
|
|
||||||
Календарь охватывает пять прошлых лет и следующий год. Будущие купоны
|
Календарь охватывает пять прошлых лет и следующий год. Будущие купоны
|
||||||
сортируются по дате выплаты, прошедшие и недатированные доступны отдельно.
|
сортируются по дате выплаты, прошедшие и недатированные доступны отдельно.
|
||||||
Показаны сумма на облигацию, номер, тип, длительность периода и дата реестра,
|
Будущие и прошедшие события купонного графика показываются единым предметным
|
||||||
|
представлением выплаты с пометкой события по графику — тем же, что у
|
||||||
|
исполненных выплат и прогноза; исполнение подтверждает только личная история.
|
||||||
|
Показаны сумма на облигацию, отдельная личная оценка, номер, тип, длительность
|
||||||
|
периода и дата реестра,
|
||||||
если они известны. Неопределённый будущий купон не заменяется нулём.
|
если они известны. Неопределённый будущий купон не заменяется нулём.
|
||||||
|
|
||||||
Личная оценка доступна только при положительных позициях, известной сумме,
|
Личная оценка доступна только при положительных позициях, известной сумме,
|
||||||
@@ -111,22 +115,24 @@ HTML-источник, общий кэш и поведение при измен
|
|||||||
транспортного контракта задаёт [HTTP API](../../architecture/http-api.md).
|
транспортного контракта задаёт [HTTP API](../../architecture/http-api.md).
|
||||||
|
|
||||||
`BondBy`, `FindInstrument` и `GetBondCoupons` используют способ
|
`BondBy`, `FindInstrument` и `GetBondCoupons` используют способ
|
||||||
декодирования, сохраняющий отсутствие полей. Рыночные RPC проходят
|
декодирования, сохраняющий отсутствие полей. Общие данные выпуска, quotes,
|
||||||
общие TLS, очередь, ограничение частоты, deadline и повторные попытки. Кэш успешных и совместных
|
календари, MOEX metrics и необходимые результаты НКР сохраняются в SQLite.
|
||||||
незавершённых запросов ограничен LRU и сроком 30 секунд по пользователю,
|
Личные позиции и оценки выплат используют текущий доступ и сохранённый
|
||||||
поколению подключения и параметрам. Ошибки не кэшируются; ошибки авторизации
|
portfolio; они не попадают в общие источники.
|
||||||
не подавляются при дополнительной загрузке данных поиска.
|
|
||||||
Исходный ответ `BondBy` совместно используется поиском, карточкой, котировкой,
|
|
||||||
графиком, купонами и сводкой показателей в пределах того же пользователя, поколения и TTL.
|
|
||||||
Одновременная загрузка секций вызывает один `BondBy` на UID. Календарь
|
|
||||||
и сводка совместно используют один запрос купонов; котировка переиспользуется
|
|
||||||
при расчёте купонной доходности. MOEX и НКР загружаются только для открытого
|
|
||||||
выпуска, независимо от поиска. Для 30 новых
|
|
||||||
результатов поиск выполняет 32 RPC до повторных попыток: один `FindInstrument`,
|
|
||||||
30 `BondBy` и один пакетный запрос цен. Общий ограничитель backend не заменяет
|
|
||||||
квоты Т-Банка; при их исчерпании сохраняется `TBANK_UNAVAILABLE`.
|
|
||||||
|
|
||||||
Разделы загружаются независимо, показывают каркас загрузки и локальную повторную попытку.
|
BondBy переиспользуется зависимостями выпуска. Discovery принадлежит query,
|
||||||
|
свежесть — конкретному source/range. Missing/stale зависимости ставятся
|
||||||
|
в фоновую очередь через прежний limiter; обычный HTTP-ответ не ждёт брокера.
|
||||||
|
Покрытые завершённые свечи не перечитываются после restart. Открытый хвост
|
||||||
|
остаётся изменяемым; ручной refresh охватывает показанный период.
|
||||||
|
|
||||||
|
Последняя полная личная оценка сохраняется при ошибке новых обязательных
|
||||||
|
зависимостей, вместе с исходным временем. Неполнота не означает отсутствие
|
||||||
|
выплат. Наблюдение статуса не запускает повторный импорт. Политика источников
|
||||||
|
и границы — в [модели свежести](../../architecture/cross-cutting/data-freshness.md).
|
||||||
|
|
||||||
|
Разделы загружаются независимо, показывают каркас загрузки и локальное
|
||||||
|
спокойное состояние недоступности без действия повтора.
|
||||||
Ошибка фонового обновления сохраняет последние данные. Смена подключения
|
Ошибка фонового обновления сохраняет последние данные. Смена подключения
|
||||||
сбрасывает поддерево клиентских запросов облигаций вместе с остальными финансовыми запросами;
|
сбрасывает поддерево клиентских запросов облигаций вместе с остальными финансовыми запросами;
|
||||||
выход очищает кэш сессии. Общие график, позиция и личные события
|
выход очищает кэш сессии. Общие график, позиция и личные события
|
||||||
@@ -142,3 +148,9 @@ HTML-источник, общий кэш и поведение при измен
|
|||||||
и [проверками](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/instruments/bonds/application/bond.spec.ts).
|
и [проверками](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/instruments/bonds/application/bond.spec.ts).
|
||||||
Границы источников описаны в [разделе интеграций](../../integrations/README.md),
|
Границы источников описаны в [разделе интеграций](../../integrations/README.md),
|
||||||
сходное разделение рыночных и личных данных — в [акциях](stocks.md).
|
сходное разделение рыночных и личных данных — в [акциях](stocks.md).
|
||||||
|
|
||||||
|
## Online reference
|
||||||
|
|
||||||
|
Страница облигации, поиск и переход по тикеру обращаются к онлайн-справочнику
|
||||||
|
брокера; сохранённые quotes, свечи, события, ratings и метки портфеля
|
||||||
|
сохраняют собственные границы свежести и ошибок.
|
||||||
|
|||||||
@@ -28,24 +28,30 @@
|
|||||||
на frontend. Общее для акций и облигаций восстановление справочных данных
|
на frontend. Общее для акций и облигаций восстановление справочных данных
|
||||||
принадлежит [загрузке портфеля на backend](../portfolio/flows/portfolio-loading.md).
|
принадлежит [загрузке портфеля на backend](../portfolio/flows/portfolio-loading.md).
|
||||||
|
|
||||||
Раздел «Акции» в боковой и мобильной навигации ведёт на `/stocks`: поиск
|
Раздел «Акции» в боковой и мобильной навигации ведёт на `/stocks`: скринер
|
||||||
по названию или тикеру через каталог Т-Банка, начиная с двух символов.
|
запускается кнопкой «Найти» с необязательным названием или тикером, сектором,
|
||||||
Результаты разных выпусков, включая обыкновенные и привилегированные акции,
|
типом акции, валютой расчётов, площадкой, страной риска и максимальным размером
|
||||||
имеют отдельные UID. Карточка результата показывает логотип, последнюю цену
|
лота. Он использует полный полученный каталог и показывает только акции, для
|
||||||
с её временем, тип бумаги, площадку, код режима, ISIN и доступность торгов через
|
которых каталог подтверждает доступность покупки и торговли через API. Это не
|
||||||
API. Поиск показывает одну карточку на нормализованный ISIN. До ограничения
|
гарантирует, что конкретная заявка пользователя будет исполнена прямо сейчас.
|
||||||
в 30 результатов выбирается представитель выпуска: доступность через API,
|
|
||||||
точное совпадение тикера, затем режим TQBR; равные варианты упорядочены по UID.
|
Скринер показывает одну карточку на выпуск с нормализованным ISIN, а выпуск
|
||||||
Записи без ISIN сохраняются отдельно по UID. Обыкновенные и привилегированные
|
без ISIN остаётся отдельной карточкой точного UID. Обыкновенные и
|
||||||
акции с разными ISIN не объединяются. Цена и ссылка принадлежат выбранному
|
привилегированные акции с разными ISIN не объединяются. После отбора карточки
|
||||||
UID; данные разных режимов не смешиваются. Прямые ссылки из портфеля сохраняют
|
упорядочены по тикеру и UID, разделены на страницы по 30 и показывают единое
|
||||||
|
время получения списка. Изменение условий не заменяет текущий список до
|
||||||
|
следующего нажатия «Найти». Повторный поиск получает новый каталог; страницы
|
||||||
|
одного результата не пересчитываются. Подтверждённый пустой результат отличается
|
||||||
|
от ошибки источника: она сообщается рядом с полем поиска, а повторить запрос
|
||||||
|
пользователь может обычным «Найти» — отдельного действия повтора чтения нет.
|
||||||
|
|
||||||
|
Ссылка карточки принадлежит выбранному UID; данные разных режимов не смешиваются.
|
||||||
|
Прямые ссылки из портфеля сохраняют
|
||||||
исходный UID, даже если поиск выбрал другой режим этого выпуска; такая страница
|
исходный UID, даже если поиск выбрал другой режим этого выпуска; такая страница
|
||||||
сохраняет UUID в адресе. Если один точный тикер относится к разным выпускам,
|
сохраняет UUID в адресе. Если один точный тикер относится к разным выпускам,
|
||||||
сервер возвращает `409 STOCK_TICKER_AMBIGUOUS`, а страница предлагает поиск.
|
сервер возвращает `409 STOCK_TICKER_AMBIGUOUS`.
|
||||||
Поиск тикера нечувствителен к регистру, но использует точное совпадение:
|
|
||||||
SBER не заменяется на SBERP. Однобуквенные тикеры также поддерживаются.
|
|
||||||
Прямая ссылка не требует предварительного открытия портфеля. Хлебные крошки
|
Прямая ссылка не требует предварительного открытия портфеля. Хлебные крошки
|
||||||
«Акции → название» возвращают к поиску.
|
«Акции → название» возвращают к скринеру.
|
||||||
|
|
||||||
## Содержание и состояния
|
## Содержание и состояния
|
||||||
|
|
||||||
@@ -58,8 +64,9 @@ SBER не заменяется на SBERP. Однобуквенные тикер
|
|||||||
рыночные данные и личная история остаются доступны.
|
рыночные данные и личная история остаются доступны.
|
||||||
|
|
||||||
Каждый раздел загружается независимо. Первичная загрузка показывает каркас
|
Каждый раздел загружается независимо. Первичная загрузка показывает каркас
|
||||||
раздела, ошибка — локальную повторную попытку. При ошибке фонового обновления
|
раздела, затянувшаяся недоступность — локальное спокойное состояние без
|
||||||
сохраняются предыдущие данные и предупреждение. Цена содержит собственное время
|
повторной попытки. При ошибке фонового обновления сохраняются предыдущие
|
||||||
|
данные без технического предупреждения. Цена содержит собственное время
|
||||||
UTC, которое может быть старше времени получения ответа. Статус торгов не
|
UTC, которое может быть старше времени получения ответа. Статус торгов не
|
||||||
выводится из свежести цены. Отсутствующее значение не заменяется нулём.
|
выводится из свежести цены. Отсутствующее значение не заменяется нулём.
|
||||||
|
|
||||||
@@ -73,7 +80,8 @@ Frontend не получает брокерский токен и не обра
|
|||||||
|
|
||||||
Личная история использует существующий
|
Личная история использует существующий
|
||||||
`GET /api/v1/brokerage-accounts/all/operations?instrumentUid=...`.
|
`GET /api/v1/brokerage-accounts/all/operations?instrumentUid=...`.
|
||||||
Фильтр UID валидируется и передаётся upstream как `instrumentId`; ответ также
|
Фильтр UID валидируется и применяется к локальной истории. При ручной
|
||||||
|
сверке бумаги он также передаётся upstream как `instrumentId`; ответ
|
||||||
проверяется по UID. Без `from` такой запрос начинается с самой ранней известной
|
проверяется по UID. Без `from` такой запрос начинается с самой ранней известной
|
||||||
даты открытия доступных счетов; если даты неполны, нижняя граница — Unix epoch.
|
даты открытия доступных счетов; если даты неполны, нижняя граница — Unix epoch.
|
||||||
Обычный журнал без фильтра инструмента сохраняет начало текущего года.
|
Обычный журнал без фильтра инструмента сохраняет начало текущего года.
|
||||||
@@ -118,7 +126,11 @@ UID включён в ключ кэша и подпись общего курс
|
|||||||
|
|
||||||
## Дивиденды и финансовые показатели
|
## Дивиденды и финансовые показатели
|
||||||
|
|
||||||
События компании отделены от личных зачислений. Известные будущие выплаты
|
События компании отделены от личных зачислений. Будущие и прошедшие события
|
||||||
|
дивидендного графика показываются единым предметным представлением выплаты
|
||||||
|
с пометкой «событие по графику» — тем же, что у исполненных выплат и прогноза;
|
||||||
|
сумма на акцию, отдельная личная оценка и дата реестра сохраняют свои подписи
|
||||||
|
и не выдаются за личное зачисление. Известные будущие выплаты
|
||||||
определяются по `paymentDate`, при её отсутствии — по `recordDate`; прошедшие
|
определяются по `paymentDate`, при её отсутствии — по `recordDate`; прошедшие
|
||||||
и недатированные события помещены в отдельную историю компании. Отменённые
|
и недатированные события помещены в отдельную историю компании. Отменённые
|
||||||
события исключаются. Личная оценка умножает сумму на акцию на текущее положительное
|
события исключаются. Личная оценка умножает сумму на акцию на текущее положительное
|
||||||
@@ -142,18 +154,27 @@ TTM означает последние 12 месяцев, MRQ — послед
|
|||||||
|
|
||||||
## Получение данных и ограничения
|
## Получение данных и ограничения
|
||||||
|
|
||||||
Запросы T-Bank проходят общую серверную интеграцию с TLS, лимитом частоты,
|
Общие metadata, discovery, company, quotes, свечи и календари сохраняются
|
||||||
очередью, предельным временем ожидания и повторными попытками. Проверка доступа
|
в SQLite. Скринер получает каталог online и показывает время его получения;
|
||||||
и расшифровка token выполняются только backend. Успешные ответы и совместные незавершённые запросы кэшируются
|
временная недоступность каталога не подтверждает отсутствие подходящих акций.
|
||||||
в ограниченном кэше LRU на 30 секунд по пользователю, поколению подключения и параметрам.
|
Отсутствующее enrichment не скрывает найденную бумагу. Сохранённые общие
|
||||||
Для поиска `FindInstrument` дополняется сведениями `ShareBy` через тот же
|
источники переиспользуются между пользователями, но личные позиции и выплаты
|
||||||
кэш карточки инструмента и одним пакетным `GetLastPrices` для списка. Ошибка
|
всегда проверяют доступ.
|
||||||
дополнительной загрузки или исчезнувшая карточка не скрывает исходную запись
|
|
||||||
каталога; недоступные логотипы и цены имеют заглушки. Ошибки авторизации
|
Обычное чтение не ждёт RPC; missing/stale зависимости ставятся в фоновую
|
||||||
не подавляются.
|
очередь через существующий T-Bank limiter. Часовая свежесть применяется
|
||||||
Ошибка целого запроса не кэшируется; отсутствие дополнительного источника
|
к quotes и открытому хвосту свечей; metadata/company/календари, будучи
|
||||||
обозначается в ответе. Поддерево клиентских запросов акций сбрасывается вместе с финансовыми
|
сохранёнными, не перечитываются обычным чтением.
|
||||||
данными при изменении подключения; выход очищает кэш сессии.
|
Закрытые полные свечные интервалы, включая пустые, сохраняются после restart.
|
||||||
|
Новый день не делает прежнюю незавершённую свечу полной.
|
||||||
|
|
||||||
|
Отдельного действия «Обновить» на странице нет: чтение текущего инструмента,
|
||||||
|
выбранного графика и personal-history scope выполняется автоматически.
|
||||||
|
Частичные строки видны с обозначением неполноты; неполные личные оценки
|
||||||
|
не становятся нулями. Статус и `sync=observe` не создают новый импорт.
|
||||||
|
Смена подключения и известный access loss очищают личное представление;
|
||||||
|
поздние ответы не возвращают прежние данные. Детали —
|
||||||
|
[модель свежести](../../architecture/cross-cutting/data-freshness.md).
|
||||||
|
|
||||||
Полные даты в интерфейсе имеют формат ДД.ММ.ГГГГ; ISO-формат транспорта и
|
Полные даты в интерфейсе имеют формат ДД.ММ.ГГГГ; ISO-формат транспорта и
|
||||||
временные зоны расчётов не меняются. Личная история использует общий с портфелем формат строки операции,
|
временные зоны расчётов не меняются. Личная история использует общий с портфелем формат строки операции,
|
||||||
@@ -177,3 +198,9 @@ TTM означает последние 12 месяцев, MRQ — послед
|
|||||||
[рыночным данным](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/instruments/stocks/application/stock-market.service.ts),
|
[рыночным данным](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/instruments/stocks/application/stock-market.service.ts),
|
||||||
[выплатам](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/instruments/stocks/application/stock-income.service.ts) и [проверкам](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/instruments/stocks/application/stock.spec.ts).
|
[выплатам](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/instruments/stocks/application/stock-income.service.ts) и [проверкам](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/instruments/stocks/application/stock.spec.ts).
|
||||||
Сопоставление с облигациями — на [странице облигаций](bonds.md).
|
Сопоставление с облигациями — на [странице облигаций](bonds.md).
|
||||||
|
|
||||||
|
## Online reference
|
||||||
|
|
||||||
|
Страница акции, поиск и переход по тикеру обращаются к онлайн-справочнику
|
||||||
|
брокера; сохранённые позиции, quotes, свечи и события остаются независимыми
|
||||||
|
локальными ресурсами и не подменяются ошибкой справочника.
|
||||||
|
|||||||
@@ -13,9 +13,27 @@
|
|||||||
Исполненная операция содержит тип события, его категорию, описание,
|
Исполненная операция содержит тип события, его категорию, описание,
|
||||||
момент совершения, доступные сведения об инструменте и денежное движение.
|
момент совершения, доступные сведения об инструменте и денежное движение.
|
||||||
Запросы отменённых или ожидающих исполнения поручений не образуют этот журнал.
|
Запросы отменённых или ожидающих исполнения поручений не образуют этот журнал.
|
||||||
Суммы остаются точными десятичными строками в исходной валюте; frontend
|
Суммы остаются точными десятичными строками в исходной валюте. Frontend
|
||||||
только форматирует их. Положительное и отрицательное движение сохраняют
|
форматирует их и считает визуальный итог группы по показанным записям
|
||||||
знак. Отсутствие `payment` означает «Без движения денег», а не нулевую сумму.
|
(см. [группировку](#группировка-в-режиме-все-счета)). Положительное и отрицательное
|
||||||
|
движение сохраняют знак. Отсутствие `payment` означает «Без движения денег», а не нулевую сумму.
|
||||||
|
|
||||||
|
## Группировка в режиме «Все счета»
|
||||||
|
|
||||||
|
Только в режиме `all` записи журнала и превью обзора, которые относятся к одному
|
||||||
|
дню экрана, одному инструменту и точному типу операции, объединяются в группу.
|
||||||
|
Ключом служат день, `instrumentUid` (иначе `figi`) и тип; группа образуется при
|
||||||
|
двух и более записях, в том числе одного счёта. Группа стоит на месте первой записи.
|
||||||
|
Каждая запись остаётся отдельной строкой с названием счёта простым текстом и своей суммой.
|
||||||
|
Одиночный счёт не группируется.
|
||||||
|
|
||||||
|
Группировка выполняется после лимита: превью обзора сначала берёт пять записей,
|
||||||
|
затем группирует только их. Полный журнал группирует страницу, уже загруженную
|
||||||
|
курсором; группы не переносятся между страницами. Курсор и порядок записей не меняются.
|
||||||
|
|
||||||
|
Итог группы — точная сумма по каждой валюте, без конвертации. Он показывается только
|
||||||
|
если полнота журнала `complete`, у каждой записи группы есть `payment`, и ни одна сумма
|
||||||
|
не равна нулю. Иначе итог скрыт, а строки счетов остаются.
|
||||||
|
|
||||||
Категории объединяют разные события для фильтрации: сделка (`trade`),
|
Категории объединяют разные события для фильтрации: сделка (`trade`),
|
||||||
доход (`income`), налог (`tax`), комиссия (`fee`), перевод (`transfer`)
|
доход (`income`), налог (`tax`), комиссия (`fee`), перевод (`transfer`)
|
||||||
@@ -46,8 +64,9 @@ ID операций разных счетов не означают дублик
|
|||||||
|
|
||||||
На [страницах акций](../instruments/stocks.md) и
|
На [страницах акций](../instruments/stocks.md) и
|
||||||
[облигаций](../instruments/bonds.md) этот же журнал становится личной историей
|
[облигаций](../instruments/bonds.md) этот же журнал становится личной историей
|
||||||
конкретной бумаги. Фильтр `instrumentUid` передаётся брокеру и проверяется
|
конкретной бумаги. Фильтр `instrumentUid` применяется к локальным операциям. Ручная сверка
|
||||||
в ответе. Без явного начала периода он охватывает историю с открытия счёта;
|
личной истории может ограничить источниковый запрос этой бумагой, не присваивая
|
||||||
|
такому subset полноту всех операций счёта. Без явного начала периода он охватывает историю с открытия счёта;
|
||||||
в общем режиме — с самой ранней известной даты открытия доступных счетов.
|
в общем режиме — с самой ранней известной даты открытия доступных счетов.
|
||||||
Если даты открытия неполны, нижняя граница — Unix epoch. Это позволяет
|
Если даты открытия неполны, нижняя граница — Unix epoch. Это позволяет
|
||||||
видеть прошлые операции уже проданной бумаги, но не обещает историю
|
видеть прошлые операции уже проданной бумаги, но не обещает историю
|
||||||
@@ -55,50 +74,38 @@ ID операций разных счетов не означают дублик
|
|||||||
|
|
||||||
## Получение и полнота
|
## Получение и полнота
|
||||||
|
|
||||||
```mermaid
|
`GET /api/v1/brokerage-accounts/:accountId/operations` читает сохранённые
|
||||||
sequenceDiagram
|
операции и coverage после локальной проверки доступа. Период, категории,
|
||||||
participant F as Frontend
|
UID, cursor и limit применяются к этой выборке. Транспорт описан в
|
||||||
participant B as Backend
|
[HTTP API](../../architecture/http-api.md).
|
||||||
participant T as T-Bank
|
|
||||||
F->>B: Запрашивает применённый период и страницу операций
|
|
||||||
B->>B: Проверяет сессию и текущее подключение
|
|
||||||
B->>T: Получает открытые счета
|
|
||||||
T-->>B: Подтверждает доступность счёта
|
|
||||||
alt Подходящая страница есть в кэше
|
|
||||||
B-->>F: Сохранённая страница
|
|
||||||
else Нужна загрузка
|
|
||||||
B->>T: GetOperationsByCursor, только исполненные операции
|
|
||||||
T-->>B: Страница операций
|
|
||||||
B->>B: Нормализует записи и применяет фильтры
|
|
||||||
B-->>F: Записи и продолжение либо ошибка
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
`GET /api/v1/brokerage-accounts/:accountId/operations` требует Bearer token.
|
Стабильность broker operation ID в пределах счёта и подключения принята как
|
||||||
Период, категории, необязательный UID, курсор и размер страницы задают запрос;
|
допущение проекта. Повторный ID обновляет ту же запись без двойного учёта.
|
||||||
ответ содержит записи, время наблюдения и сведения о продолжении.
|
Страницы источника сначала сохраняются в staging. Полный успешный обход
|
||||||
Точные поля и допустимые значения задаёт [HTTP API](../../architecture/http-api.md).
|
разрешает upsert, удаление исчезнувших записей в перечитанном predicate
|
||||||
|
и публикацию coverage одной транзакцией. При повторной сверке полного
|
||||||
|
показанного периода транзакция охватывает все его окна и счета.
|
||||||
|
|
||||||
Backend хранит не более 100 страниц в памяти процесса. Свежая страница
|
Полнота относится к UTC-интервалу `[from,to)`, включая подтверждённый пустой.
|
||||||
живёт до 30 секунд; при переполнении вытесняется давно не использовавшаяся.
|
Категории, размер страницы и `hasNext=false` её не определяют. При первичном
|
||||||
Ключ разделяет пользователя, поколение подключения, счёт, нормализованный
|
импорте подтверждённые строки видны как partial; итоги ждут полного периода.
|
||||||
период и категории, UID инструмента, курсор и размер страницы.
|
Догрузка остальной истории обозначается отдельно. Без известной нижней
|
||||||
Отсутствующий конец периода приводится к текущему 30-секундному интервалу.
|
границы нельзя заявить полную историю счёта.
|
||||||
Текущее подключение и доступ к счёту проверяются до выдачи страницы из кэша.
|
|
||||||
|
|
||||||
После успешного изменения подключения страницы предыдущего поколения,
|
Cursor связан с пользователем, подключением, membership для `all`, фильтрами
|
||||||
в том числе поздно завершившиеся запросы, не выдаются журналу или аналитике
|
и ревизией набора операций. Изменение строк даёт `INVALID_OPERATIONS_CURSOR`;
|
||||||
дохода. Они удаляются по обычным ограничениям времени и объёма.
|
клиент возвращается на первую страницу, сохраняя применённые фильтры.
|
||||||
Локальной синхронизации и долговременного хранения операций нет.
|
Coverage/status без изменения rowset cursor не ломают. Перезапуск сохраняет
|
||||||
Продолжения [общего журнала](../portfolio/all-accounts.md#общая-пагинация-операций)
|
опубликованные интервалы; незавершённая работа возобновляется только запросом.
|
||||||
имеют собственные ограничения и восстановление после истечения.
|
|
||||||
|
|
||||||
## Представление и ошибки
|
## Представление и ошибки
|
||||||
|
|
||||||
Полный журнал и превью используют одинаковую компактную строку: логотип
|
Полный журнал и превью используют одинаковую компактную строку: логотип
|
||||||
или буквенная заглушка, описание, дата, инструмент и денежное движение.
|
или буквенная заглушка, описание, дата, инструмент и денежное движение.
|
||||||
Для купона и дивиденда денежная метка называет точное событие; для прочих
|
Исполненные купоны и дивиденды показываются единым предметным представлением
|
||||||
операций — категорию. Она показывается один раз, без повтора рядом с датой.
|
выплаты — тем же, что на странице дохода, в прогнозе и графиках инструментов;
|
||||||
|
денежная метка называет точное исполненное событие. Для прочих
|
||||||
|
операций метка называет категорию. Она показывается один раз, без повтора рядом с датой.
|
||||||
Знак и цвет обозначают направление денег; отсутствие движения нейтрально.
|
Знак и цвет обозначают направление денег; отсутствие движения нейтрально.
|
||||||
Если описание отсутствует, показывается локализованный тип операции.
|
Если описание отсутствует, показывается локализованный тип операции.
|
||||||
|
|
||||||
@@ -110,7 +117,8 @@ Backend хранит не более 100 страниц в памяти проц
|
|||||||
не заменяется каркасом при применении действий или фоновом обновлении.
|
не заменяется каркасом при применении действий или фоновом обновлении.
|
||||||
Ошибки `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, недоступный счёт (`404`)
|
Ошибки `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, недоступный счёт (`404`)
|
||||||
и `TBANK_UNAVAILABLE` сохраняют [общую семантику API](../../architecture/http-api.md).
|
и `TBANK_UNAVAILABLE` сохраняют [общую семантику API](../../architecture/http-api.md).
|
||||||
Пустой успешный ответ означает отсутствие подходящих операций, а не сбой.
|
Только полный пустой ответ означает отсутствие подходящих операций.
|
||||||
|
Partial empty сообщает о неполноте; source failure сохраняет прежний результат.
|
||||||
|
|
||||||
Согласованное поведение описано в [спецификации журнала](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-ledger/spec.md).
|
Согласованное поведение описано в [спецификации журнала](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-ledger/spec.md).
|
||||||
Фильтр конкретной бумаги и общий режим расширяют исходное описание отдельного
|
Фильтр конкретной бумаги и общий режим расширяют исходное описание отдельного
|
||||||
|
|||||||
@@ -51,69 +51,62 @@ Backend публикует статические маршруты под `/api/
|
|||||||
используют прежнюю карточку. Доля относится к позициям той же валюты
|
используют прежнюю карточку. Доля относится к позициям той же валюты
|
||||||
во всём показанном портфеле, без денежных остатков.
|
во всём показанном портфеле, без денежных остатков.
|
||||||
- Журнал объединяет операции по убыванию времени, при равном времени использует
|
- Журнал объединяет операции по убыванию времени, при равном времени использует
|
||||||
порядок идентификаторов счетов, сохраняя порядок внутри одного счёта. ID
|
порядок идентификаторов счетов и операций. ID
|
||||||
операций разных счетов не считаются дублями. Переводы не схлопываются.
|
операций разных счетов не считаются дублями. Переводы не схлопываются.
|
||||||
- Доходы собираются из всех страниц операций каждого счёта за общие периоды.
|
- Доходы собираются из всех страниц операций каждого счёта за выбранный период.
|
||||||
Итоги, сравнения, временные интервалы и принадлежность выплат рассчитывает
|
Итоги, временные интервалы и принадлежность выплат рассчитывает
|
||||||
сервис дохода.
|
сервис дохода. Данные сравнения периодов не загружаются и не возвращаются;
|
||||||
Для сравнения с годом ранее используется текущий состав счетов; новый счёт
|
состав счетов влияет только на фактические выплаты выбранного периода.
|
||||||
добавляет только реально полученные выплаты. При известных датах открытия всех счетов
|
|
||||||
доступность сравнения определяется самым ранним открытием в этом составе.
|
|
||||||
Закрытые счета в историческое сравнение не добавляются.
|
|
||||||
- Будущие события сортируются по дате и сохраняют отдельную строку каждого счёта.
|
- Будущие события сортируются по дате и сохраняют отдельную строку каждого счёта.
|
||||||
Прогноз не включается в реализованные доходы.
|
Прогноз не включается в реализованные доходы.
|
||||||
|
- [Результат инвестирования](investment-performance.md) складывает потоки и
|
||||||
|
стоимость только рублёвых счетов с полной историей. Счёт с причиной
|
||||||
|
недоступности исключается, сводка помечается неполной. Похожие на перевод
|
||||||
|
между своими счетами операции без подтверждённой связи исключают оба счёта
|
||||||
|
(`UNCONFIRMED_TRANSFER`). Если оценки оставшихся счетов сделаны в разные
|
||||||
|
московские дни, сводка недоступна: стоимости разных дат не складываются.
|
||||||
|
|
||||||
## Полнота, свежесть и доступ
|
## Полнота, свежесть и доступ
|
||||||
|
|
||||||
Для каждого общего запроса backend определяет состав доступных счетов
|
Общий запрос читает membership и разрешённые snapshots из SQLite в одной
|
||||||
и единое поколение подключения. Получение данных использует эту же границу подключения;
|
согласованной границе. Время источников может различаться: это не обещание
|
||||||
проверки сессии, доступа и существующий лимитер внешних запросов сохраняются.
|
одновременного наблюдения счетов у брокера. Неполный счёт делает aggregate
|
||||||
Финансовые данные не сохраняются в БД; снимки и продолжения хранятся
|
неполным; отсутствующие суммы не подменяются нулями. `lastSuccessfulAt`
|
||||||
в памяти процесса.
|
показывает самое раннее время обязательных источников.
|
||||||
|
|
||||||
Ошибка источника не превращается в частичный успешный ответ раздела.
|
Секции портфеля, операций, дохода и событий готовы независимо. Ошибка
|
||||||
Портфель может использовать разрешённый существующей политикой запасной снимок
|
обновления сохраняет прежний полный результат того же scope и периода.
|
||||||
отдельного счёта. Тогда общий `isStale` равен `true`, а `lastSuccessfulAt`
|
Известный отказ доступа и смена подключения очищают personal view.
|
||||||
показывает самое раннее время среди включённых снимков. Для пустого состава
|
|
||||||
возвращается корректный пустой результат. Общий запрос не является атомарным
|
|
||||||
наблюдением всех счетов в один момент времени.
|
|
||||||
|
|
||||||
Ошибка блока операций, доходов или будущих событий не скрывает основной
|
|
||||||
портфель. При фоновом обновлении страницы уже полученный результат сохраняется
|
|
||||||
с индикатором или предупреждением и повторной попыткой. Смена сессии или
|
|
||||||
подключения сбрасывает общий режим вместе с остальными финансовыми запросами.
|
|
||||||
|
|
||||||
## Общая пагинация операций
|
## Общая пагинация операций
|
||||||
|
|
||||||
Сервис объединяет потоки страниц каждого счёта и сохраняет непрочитанные строки
|
Страница строится из локальных строк с порядком occurredAt, accountId и ID
|
||||||
между запросами. Фильтрованные пустые страницы не обрывают поток. Повреждённая
|
операции. Cursor проверяет пользователя, подключение, membershipRevision,
|
||||||
цепочка курсоров и нарушенный порядок дат возвращают ошибку без частичной страницы.
|
фильтры, limit и ревизии операций. Изменение rowset возвращает
|
||||||
|
`400 INVALID_OPERATIONS_CURSOR`; клиент сбрасывает стек страниц, сохраняя
|
||||||
|
применённые фильтры. Coverage/status без изменения строк не ломают cursor.
|
||||||
|
Продолжение не зависит от RAM-кэша или перезапуска.
|
||||||
|
|
||||||
Курсор общего журнала — случайный непрозрачный идентификатор продолжения,
|
Визуальная группировка строк одного инструмента в общих списках выполняется только
|
||||||
привязанного к пользователю, поколению подключения, составу счетов, фильтрам
|
в пределах уже полученного списка: в журнале — на загруженной странице, без переноса группы
|
||||||
и размеру страницы. Верхняя граница периода фиксируется первым запросом.
|
на следующую; в полном списке будущих событий и выплатах дохода — по всем полученным строкам
|
||||||
Состояние ограничено 100 продолжениями на процесс со сроком жизни пять минут;
|
периода. Группировка не меняет порядок, cursor или ответ API. Протокол курсора прежний.
|
||||||
повторный запрос того же продолжения возвращает тот же успешный результат.
|
Правила группировки — в [журнале](../operations/overview.md#группировка-в-режиме-все-счета).
|
||||||
|
|
||||||
Неизвестный, вытесненный, истёкший либо несовместимый курсор возвращает
|
Повторная сверка полного показанного периода публикует все счета и интервалы
|
||||||
`400 INVALID_OPERATIONS_CURSOR`. Действие «Повторить» сбрасывает пагинацию,
|
одной транзакцией; успешное первое окно не смешивается с прежними остальными.
|
||||||
обновляет первую страницу и сохраняет фильтры. Перезапуск backend также
|
Подробнее — [журнал](../operations/overview.md#получение-и-полнота).
|
||||||
инвалидирует продолжения. Раздел рассчитан на один экземпляр backend, как
|
|
||||||
существующий финансовый кэш.
|
|
||||||
|
|
||||||
## Совместимость и границы требований
|
## Совместимость и границы требований
|
||||||
|
|
||||||
Общий режим добавляет маршруты, сохраняя маршруты отдельных счетов.
|
Общий режим использует те же persistent sources и envelope, что и отдельный
|
||||||
При совместном обновлении сначала нужен backend, затем frontend с
|
счёт. Новый envelope несовместим с прежним bare response: backend и frontend
|
||||||
соответствующим сгенерированным контрактом; прежний клиент продолжает
|
поставляются согласованной парой по [HTTP contract](../../architecture/http-api.md).
|
||||||
работать с отдельными счетами. При откате сначала возвращается клиент.
|
Additive migration сохраняет прежние таблицы; rollback приложения не удаляет
|
||||||
Этот механизм не требует изменения схемы SQLite и миграции финансовых данных.
|
новые данные и не заменяет рабочую базу старым backup. Возврат требует заранее
|
||||||
|
проверенной совместимой пары; произвольный прежний клиент не считается совместимым.
|
||||||
|
Процедура backup/restore описана в [runbook](../../architecture/deployment.md).
|
||||||
|
|
||||||
Основные спецификации [портфеля](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md),
|
Основные спецификации областей сохраняют исходные понятия отдельного счёта.
|
||||||
[операций](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-ledger/spec.md),
|
Утверждённый change `persist-tbank-data` распространяет полноту, локальный
|
||||||
[дохода](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-income-analytics/spec.md)
|
доступ, атомарную сверку и состояния синхронизации на режим `all`.
|
||||||
и [событий](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-upcoming-events/spec.md)
|
|
||||||
описывают отдельный счёт. Общий режим подтверждён действующими маршрутизаторами,
|
|
||||||
сервисами и [проверками агрегации](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/all-accounts.spec.ts),
|
|
||||||
но не выделен в самостоятельную основную спецификацию. Не следует считать
|
|
||||||
описание общего режима изменением требований к отдельному счёту.
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Концентрация портфеля
|
||||||
|
|
||||||
|
Вкладка «Концентрация» на странице счёта `/accounts/:accountId` показывает,
|
||||||
|
какие акции, облигации, секторы и эмитенты занимают наибольшую часть текущей
|
||||||
|
оценки Portfolio. Она отвечает на вопрос «где сконцентрированы деньги»,
|
||||||
|
который не решают карточки позиций и [структура
|
||||||
|
активов](overview.md#как-читать-экран). Точный смысл полей и долей — в
|
||||||
|
[справочнике данных](data-reference.md).
|
||||||
|
|
||||||
|
## Вкладка и разрезы
|
||||||
|
|
||||||
|
Вкладка есть только у отдельного счёта: в режиме [«Все счета»](all-accounts.md)
|
||||||
|
её нет. Появление вкладки не меняет вкладку, открытую по умолчанию («Позиции»),
|
||||||
|
и содержание остальных вкладок. Пока Portfolio загружается, при его ошибке или
|
||||||
|
штатном пустом составе вкладки нет; при Portfolio хотя бы с одной позицией или
|
||||||
|
денежным остатком вкладка показывается.
|
||||||
|
|
||||||
|
Вкладка содержит четыре разреза и показывает один из них одновременно:
|
||||||
|
|
||||||
|
| Разрез | Что ранжирует | Типы позиций |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| «Акции» | Пять крупнейших позиций акций | `STOCK` |
|
||||||
|
| «Облигации» | Пять крупнейших позиций облигаций | `BOND` |
|
||||||
|
| «Секторы» | Пять крупнейших секторов по данным T-Bank | `STOCK`, `BOND`, `FUND` |
|
||||||
|
| «Эмитенты» | Пять крупнейших эмитентов (брендов актива) | `STOCK`, `BOND` |
|
||||||
|
|
||||||
|
Фонды участвуют только в разрезе «Секторы»: их эмитент — управляющая компания,
|
||||||
|
а не компания внутри фонда. Позиции типов `CURRENCY`, `FUTURE`, `OPTION`,
|
||||||
|
`STRUCTURED_NOTE`, `DIGITAL_FINANCIAL_ASSET` и `OTHER` во вкладку не входят.
|
||||||
|
|
||||||
|
Позиции без сектора образуют явную группу «Сектор не указан», без бренда —
|
||||||
|
«Эмитент не указан»; обе группы показываются с видимой долей. Государственные
|
||||||
|
облигации определяются значением сектора T-Bank `government` с подписью
|
||||||
|
«Государственный сектор»; других собственных классификаций WealthLedger
|
||||||
|
не вводит. Название элемента в разрезе эмитентов — название бренда; если у
|
||||||
|
одного бренда разные названия, показывается название позиции с наибольшей
|
||||||
|
стоимостью.
|
||||||
|
|
||||||
|
## Топ-5 и «Прочее»
|
||||||
|
|
||||||
|
В каждом разрезе показаны пять крупнейших элементов по текущей денежной оценке,
|
||||||
|
каждый с суммой и долей. Рядом показан суммарный показатель «Топ-5 · N %».
|
||||||
|
Если в разрезе семь и более элементов, все остальные объединены в одну строку
|
||||||
|
«Прочее» с числом элементов, суммой и долей. При шести и меньшем числе
|
||||||
|
элементов каждый показан своей строкой. По нажатию на «Прочее» раскрывается
|
||||||
|
полный перечень остальных элементов, каждый со своей суммой и долей; топ-5
|
||||||
|
и диаграмма при этом не меняются.
|
||||||
|
|
||||||
|
Доли считаются от суммы стоимостей позиций разреза в выбранной валюте и
|
||||||
|
в сумме дают ровно 100 %; округление — до десятой доли процента, очень малые
|
||||||
|
доли показываются как «< 0,1 %». Отрицательные и нулевые стоимости, а также
|
||||||
|
позиции без оценки в разрез не входят. Конвертации валют нет: каждая позиция
|
||||||
|
входит только в валюту своей оценки.
|
||||||
|
|
||||||
|
## Валюта
|
||||||
|
|
||||||
|
У вкладки свой список валют — валюты оценок позиций акций, облигаций и фондов,
|
||||||
|
отсортированные по ISO-коду, — и свой выбор, независимый от «Структуры
|
||||||
|
портфеля». По умолчанию выбрана первая валюта списка. При одной валюте
|
||||||
|
переключатель не показывается. Общего итога по нескольким валютам нет.
|
||||||
|
Выбранные разрез и валюта сохраняются, пока пользователь остаётся на странице
|
||||||
|
счёта, включая переключение её вкладок, и сбрасываются при новом заходе и при
|
||||||
|
смене счёта.
|
||||||
|
|
||||||
|
## Диаграмма и неполные данные
|
||||||
|
|
||||||
|
Рядом с перечнем показана кольцевая диаграмма, согласованная с перечнем;
|
||||||
|
информация передаётся не только цветом, есть текстовый эквивалент. Если ответ
|
||||||
|
отмечен как неполный (`completeness: "incomplete"` — часть позиций не имеет
|
||||||
|
оценки), над разрезом появляется предупреждение «Данные неполные», а под
|
||||||
|
списком — сноска «Не учтено позиций: N» с числом позиций, не вошедших в разрез.
|
||||||
|
Пустой разрез (нет позиций с положительной оценкой в выбранной валюте)
|
||||||
|
показывает текстовое состояние вместо диаграммы.
|
||||||
|
|
||||||
|
## Откуда берутся сектор и эмитент
|
||||||
|
|
||||||
|
Сектор и эмитент — часть данных Portfolio: их получает backend в часовом
|
||||||
|
обходе и сохраняет в снимке вместе с остальными полями позиций
|
||||||
|
([поток загрузки](flows/portfolio-loading.md#обновление-сектора-и-эмитента),
|
||||||
|
[интеграция T-Bank](../../integrations/tbank.md)). Вкладка считается по уже
|
||||||
|
загруженному снимку и не обращается к T-Bank.
|
||||||
@@ -32,7 +32,9 @@
|
|||||||
| Cash Balance | Суммы из `GetPositions.money`, объединённые по валюте | Заблокированные деньги не прибавляются; торгуемый инструмент `CURRENCY` остаётся позицией |
|
| Cash Balance | Суммы из `GetPositions.money`, объединённые по валюте | Заблокированные деньги не прибавляются; торгуемый инструмент `CURRENCY` остаётся позицией |
|
||||||
| `totalsByCurrency` | Стоимости позиций + доступные деньги той же валюты | Не включает невычислимую стоимость; не обещает полную оценку портфеля |
|
| `totalsByCurrency` | Стоимости позиций + доступные деньги той же валюты | Не включает невычислимую стоимость; не обещает полную оценку портфеля |
|
||||||
| `lastSuccessfulAt` | Время формирования успешного снимка, UTC | Не является временем каждой котировки; запасной снимок сохраняет исходное время |
|
| `lastSuccessfulAt` | Время формирования успешного снимка, UTC | Не является временем каждой котировки; запасной снимок сохраняет исходное время |
|
||||||
| `isStale` | Backend вернул предыдущий полный снимок после временной ошибки | Не описывает состояние клиентского кэша или ошибку фонового запроса |
|
| `isStale` | Истёк часовой срок свежести сохранённого portfolio source | Не описывает состояние клиентского кэша или ошибку фонового запроса |
|
||||||
|
| `sector` | Сектор инструмента в том виде, как его определяет T-Bank, с обрезанными пробелами | Только для позиций `STOCK`, `BOND` и `FUND`; значение из одних пробел считается отсутствующим. Во всех остальных случаях — `null` |
|
||||||
|
| `issuer` | Бренд актива инструмента по данным T-Bank: `{ id, name }` | Только для позиций `STOCK`, `BOND` и `FUND`, когда у актива известны uid и название бренда. Во всех остальных случаях — `null`. Не влияет на количество, стоимость и свежесть снимка; используется вкладкой [«Концентрация»](concentration.md) |
|
||||||
|
|
||||||
Ненулевые денежные остатки одной валюты суммируются. Позиции одинаковых
|
Ненулевые денежные остатки одной валюты суммируются. Позиции одинаковых
|
||||||
инструментов в ответе отдельного счёта не объединяются; визуальная группировка
|
инструментов в ответе отдельного счёта не объединяются; визуальная группировка
|
||||||
@@ -44,6 +46,18 @@
|
|||||||
отдельного счёта не создают валютные карточки. Если `totalsByCurrency` пуст,
|
отдельного счёта не создают валютные карточки. Если `totalsByCurrency` пуст,
|
||||||
но позиции или деньги присутствуют, это не пустой портфель.
|
но позиции или деньги присутствуют, это не пустой портфель.
|
||||||
|
|
||||||
|
## Результат инвестирования
|
||||||
|
|
||||||
|
| Поле | Смысл | Ограничение |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `netInvested`, `currentValue`, `profit` | Чистые внешние потоки, сумма RUB-оценок, их разность | Только RUB, копейки |
|
||||||
|
| `return.value` | Доля XIRR (`0.151` = 15,1 %) | `null` с `unavailableReason`; при `basis = period` — за весь период |
|
||||||
|
| `benchmark`, `reference` | Те же потоки в MCFTRR и IMOEX | IMOEX только справка |
|
||||||
|
| `outperformance` | Разница долей и стоимостей с MCFTRR | `null`, если любая сторона недоступна |
|
||||||
|
| `series` | Точки торговых дней MCFTRR | Пусто без истории индекса |
|
||||||
|
|
||||||
|
Подробнее — [результат инвестирования](investment-performance.md).
|
||||||
|
|
||||||
## Идентификация и оформление инструмента
|
## Идентификация и оформление инструмента
|
||||||
|
|
||||||
UID определяет конкретный инструмент брокера и используется для перехода
|
UID определяет конкретный инструмент брокера и используется для перехода
|
||||||
@@ -56,8 +70,10 @@ UID определяет конкретный инструмент брокер
|
|||||||
ISIN, тикер и `logoName` могут быть `null`. `logoName` — имя файла бренда,
|
ISIN, тикер и `logoName` могут быть `null`. `logoName` — имя файла бренда,
|
||||||
а не URL: frontend строит ссылку на публичный CDN. Ошибка изображения
|
а не URL: frontend строит ссылку на публичный CDN. Ошибка изображения
|
||||||
или метаданных не меняет количество, суммы и свежесть снимка и не скрывает
|
или метаданных не меняет количество, суммы и свежесть снимка и не скрывает
|
||||||
финансовые данные. Имя ожидается перед завершением нового снимка;
|
финансовые данные. Metadata обновляется независимо; отсутствие имени не задерживает выдачу
|
||||||
ошибка поиска оставляет доступный идентификатор.
|
финансового снимка и оставляет доступный идентификатор.
|
||||||
|
`logoName` не равен эмитенту из вкладки «Концентрация»: он берётся из metadata
|
||||||
|
инструмента, а эмитент — из [классификации инструмента](flows/portfolio-loading.md#обновление-сектора-и-эмитента).
|
||||||
|
|
||||||
## Структура и доли
|
## Структура и доли
|
||||||
|
|
||||||
|
|||||||
@@ -1,158 +1,66 @@
|
|||||||
# Загрузка портфеля
|
# Загрузка портфеля
|
||||||
|
|
||||||
Поток начинается при открытии `/accounts/:accountId` пользователем
|
Экран `/accounts/:accountId` читает последний полный snapshot из SQLite после
|
||||||
с действующей сессией. Его результат — полный нормализованный снимок
|
проверки сессии, подключения и локального membership. Если список счетов ещё
|
||||||
доступного счёта либо объяснимая ошибка. Браузер обращается к backend;
|
неизвестен, сервер сообщает о загрузке доступа без выдачи личных данных
|
||||||
доступ к брокерскому токену и внешние запросы остаются на сервере.
|
неизвестного ID. Известный недоступный счёт возвращает `404`.
|
||||||
|
|
||||||
## Основной обмен
|
## Основной обмен
|
||||||
|
|
||||||
Сначала frontend проверяет свой успешный кэш: в течение 30 секунд повторное
|
Обычный GET и retry возвращают read-only локальный `{ data, meta }` envelope:
|
||||||
открытие может показать ответ без нового HTTP-запроса. Следующая схема
|
они не ждут и не вызывают T-Bank и не предлагают ручную синхронизацию. Часовой
|
||||||
описывает запрос, который действительно дошёл до backend.
|
обход текущего подключения обновляет Portfolio независимо от экрана. Для нового
|
||||||
|
полного snapshot ему нужны оба финансовых ответа `GetPortfolio` и
|
||||||
|
`GetPositions`; суммы и позиции публикуются вместе. Metadata инструмента и
|
||||||
|
классификация (сектор и эмитент) дополняют представление независимо,
|
||||||
|
с доступным UID/FIGI вместо имени до их загрузки.
|
||||||
|
|
||||||
```mermaid
|
## Обновление сектора и эмитента
|
||||||
sequenceDiagram
|
|
||||||
participant F as Frontend
|
|
||||||
participant B as Backend API
|
|
||||||
participant D as SQLite
|
|
||||||
participant T as T-Bank
|
|
||||||
F->>B: Запрос портфеля счёта с Bearer token
|
|
||||||
B->>B: Проверяет сессию
|
|
||||||
B->>D: Читает текущее подключение пользователя
|
|
||||||
D-->>B: Подключение либо отсутствие
|
|
||||||
alt Подключения нет
|
|
||||||
B-->>F: 409 TBANK_NOT_CONNECTED
|
|
||||||
else Подключение доступно
|
|
||||||
B->>B: Определяет поколение подключения
|
|
||||||
alt Снимок того же поколения не старше 30 секунд
|
|
||||||
B-->>F: Полный снимок, isStale=false
|
|
||||||
else Нужен новый снимок
|
|
||||||
B->>T: Получает открытые счета
|
|
||||||
T-->>B: Состав доступных счетов
|
|
||||||
alt Счёт недоступен
|
|
||||||
B-->>F: 404
|
|
||||||
else Счёт доступен
|
|
||||||
par Финансовые позиции
|
|
||||||
B->>T: GetPortfolio
|
|
||||||
T-->>B: Позиции и цены
|
|
||||||
and Доступные деньги
|
|
||||||
B->>T: GetPositions
|
|
||||||
T-->>B: Денежные остатки
|
|
||||||
end
|
|
||||||
B->>T: Метаданные отсутствующих инструментов по UID, затем FIGI
|
|
||||||
T-->>B: Метаданные, отсутствие или ошибка
|
|
||||||
B->>B: Нормализует состав и точно вычисляет суммы
|
|
||||||
B-->>F: Полный новый снимок, isStale=false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
Свежий серверный снимок выдаётся после разрешения текущего сохранённого
|
Часовой обход также обновляет сектор и эмитента позиций типов `share`, `bond`
|
||||||
подключения, но без нового `GetAccounts`. После истечения свежего кэша
|
и `etf`. Значения хранятся как последнее известное ([ADR
|
||||||
проверка доступности счёта предшествует финансовым RPC. SQLite хранит
|
022](../../../adr/022-stored-instrument-classification.md)): сектор — как его
|
||||||
подключение, а не снимок или право владения счётом вне этого подключения.
|
определяет T-Bank, эмитент — бренд актива (`uid` и `name`). Каждое поле
|
||||||
|
обновляется при первом обходе, начавшемся не ранее чем через 24 часа после его
|
||||||
|
последнего успешного получения; пока значение не получено, попытка повторяется
|
||||||
|
в каждом обходе. Сектор и эмитент обновляются независимо.
|
||||||
|
|
||||||
Для нового снимка требуются оба финансовых ответа. Backend не выдаёт
|
Неуспешное получение не мешает публикации snapshot и не меняет
|
||||||
позиции без денег или деньги без позиций как полный портфель.
|
`lastSuccessfulAt` и `isStale`; сохранённое значение остаётся доступным.
|
||||||
Метаданные дополняют имя и идентификаторы, но их временная недоступность
|
Исключение — отказ token: он обрабатывается так же, как отказ token при
|
||||||
не срывает финансовый результат. Поиск выполняется партиями по пять позиций,
|
обходе Portfolio. Инструмент ищется по UID, затем по FIGI; ответ «не найдено»
|
||||||
чтобы крупный портфель не переполнял очередь одновременными запросами.
|
сохраняет прежнее значение и отсчёт 24 часов начинается заново. Позиции без
|
||||||
Порядок позиций сохраняется. Backend ищет сведения по UID, затем по FIGI,
|
полученных значений участвуют в разрезах вкладки
|
||||||
если UID не дал пригодного названия или тикера.
|
[«Концентрация»](../concentration.md) как «Сектор не указан» и
|
||||||
Ожидание метаданных ограничено 30 секундами на снимок счёта. После этого
|
«Эмитент не указан».
|
||||||
используется доступный кэш либо идентификатор, новые поиски для снимка
|
|
||||||
не запускаются. Уже начатые запросы могут закончиться позже и обновить общий
|
|
||||||
справочный кэш, но не меняют выданный финансовый снимок.
|
|
||||||
Общий серверный клиент обеспечивает TLS, очередь,
|
|
||||||
ограничение частоты, deadline RPC и разрешённые повторные попытки —
|
|
||||||
[граница интеграции](../../../integrations/tbank.md).
|
|
||||||
|
|
||||||
## Время и изменение подключения
|
Открытый экран выполняет локальное перечитывание не чаще одного раза в
|
||||||
|
30 минут; retry также повторяет только HTTP GET. `data: null` означает, что
|
||||||
|
полного snapshot ещё нет, а не нулевой портфель. Пустой подтверждённый portfolio
|
||||||
|
остаётся available complete result. Секции могут быть готовы независимо.
|
||||||
|
Подробнее — [свежесть](../../../architecture/cross-cutting/data-freshness.md).
|
||||||
|
|
||||||
| Слой | Что сохраняется | Условие повторного использования |
|
## Границы доступа
|
||||||
| --- | --- | --- |
|
|
||||||
| Frontend | Последний успешный HTTP-ответ для выбранного счёта и сессии | Свежим считается 30 секунд; при обычном обновлении уже показанный ответ остаётся видимым |
|
|
||||||
| Backend | Полный снимок пары пользователь–счёт в памяти процесса | До 30 секунд, только в том же поколении текущего подключения |
|
|
||||||
| Backend при временной ошибке | Тот же полный снимок | Не старше пяти минут с момента успешного сохранения, только в том же поколении |
|
|
||||||
| Метаданные инструмента | Успешный поиск или подтверждённое отсутствие | Свежими считаются 24 часа; общий кэш на 500 ключей UID/FIGI, ошибки не сохраняются |
|
|
||||||
|
|
||||||
Это параметры текущей реализации, а не самостоятельная политика этого
|
Публикация проверяет connection revision и attempt в той же транзакции. Замена
|
||||||
документа: [снимки](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/portfolio/application/portfolio.service.ts),
|
или удаление подключения не допускает позднюю запись прежней работы. Frontend
|
||||||
[метаданные](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/integrations/tbank/instrument-metadata.service.ts) и
|
очищает недоступный personal view и отклоняет запоздалые ответы. В режиме
|
||||||
[клиентский запрос](https://git.personal.ksv741.ru/WealthLedger/frontend/src/commit/9f7e342333b80bfacfd5bc394a2dcd0613d33ff3/src/entities/portfolio/api/usePortfolio.ts).
|
[«Все счета»](../all-accounts.md#полнота-свежесть-и-доступ) состав accounts и
|
||||||
|
snapshots читается согласованно, но их source times могут различаться.
|
||||||
Пять минут — предельный возраст успешного снимка, а не дополнительные
|
|
||||||
пять минут после окончания свежих 30 секунд. Запасной ответ сохраняет
|
|
||||||
исходное `lastSuccessfulAt` и получает `isStale: true`.
|
|
||||||
|
|
||||||
После истечения 24 часов backend обновляет справочные данные. Если обновление
|
|
||||||
временно недоступно, ранее успешные сведения остаются запасным вариантом,
|
|
||||||
пока запись есть в ограниченном кэше процесса. Ошибка не продлевает срок
|
|
||||||
свежести: следующий поиск снова пытается обновить запись. Это относится только
|
|
||||||
к названию, тикеру, ISIN и логотипу, а не к ценам или денежным значениям.
|
|
||||||
Перезапуск backend и вытеснение записи из кэша удаляют этот запасной вариант.
|
|
||||||
Неполный ответ без названия и тикера также не затирает известные сведения
|
|
||||||
и не обновляет срок их свежести. Подтверждённое отсутствие инструмента
|
|
||||||
заменяет прежнюю запись отрицательным результатом поиска.
|
|
||||||
|
|
||||||
Время снимка фиксируется при получении финансовых ответов, до ожидания
|
|
||||||
названий. Сроки серверного кэша отсчитываются от этого времени, поэтому
|
|
||||||
долгая загрузка справочных данных не делает старые суммы новыми.
|
|
||||||
Успешные справочные данные общие для всех пользователей; выполняющиеся
|
|
||||||
запросы объединяются только внутри одного брокерского токена, чтобы ошибка
|
|
||||||
его авторизации не передавалась другому подключению.
|
|
||||||
|
|
||||||
Успешное создание, замена или удаление подключения меняет границу
|
|
||||||
повторного использования финансовых данных. Старое или поздно завершившееся
|
|
||||||
получение снимка не делает его доступным новым запросам другого поколения.
|
|
||||||
Явная очистка памяти при изменении подключения дополняет эту проверку.
|
|
||||||
Frontend сбрасывает всё поддерево финансовых запросов при изменении
|
|
||||||
подключения и общий кэш сессии при выходе; старый портфель не служит
|
|
||||||
заглушкой загрузки нового подключения.
|
|
||||||
|
|
||||||
В общем режиме сначала определяется состав доступных счетов одного
|
|
||||||
подключения, затем получаются их снимки. Это
|
|
||||||
[не атомарное наблюдение нескольких счетов](../all-accounts.md#полнота-свежесть-и-доступ).
|
|
||||||
|
|
||||||
## Ошибки и результат для пользователя
|
## Ошибки и результат для пользователя
|
||||||
|
|
||||||
| Причина | Ответ backend | Действие на экране |
|
Нет подключения — `409 TBANK_NOT_CONNECTED`, известный отказ токена —
|
||||||
| --- | --- | --- |
|
`400 INVALID_TBANK_TOKEN`, недоступный счёт — `404`. Они не маскируются
|
||||||
| Нет подключения | `409 TBANK_NOT_CONNECTED` | Открыть настройки T-Bank |
|
запасным snapshot. Временная ошибка часового обхода оставляет прежний полный
|
||||||
| Брокер отклонил токен | `400 INVALID_TBANK_TOKEN` | Открыть настройки; токен автоматически не удаляется |
|
portfolio и его исходное `lastSuccessfulAt`; `meta.lastAttempt` сообщает о
|
||||||
| Счёт недоступен через текущее подключение | `404` | Вернуться к списку счетов |
|
failed или partial попытке отдельно. `Portfolio.isStale` сообщает возраст
|
||||||
| Временная ошибка, пригодный предыдущий снимок есть | `200`, `isStale: true` | Показать снимок и его исходное время с предупреждением |
|
snapshot, а Client Cached Snapshot и ошибка браузерного HTTP-перечитывания
|
||||||
| Временная ошибка, пригодного снимка нет | `503 TBANK_UNAVAILABLE` | Повторить загрузку |
|
имеют независимый смысл. При отсутствии полного snapshot `data: null` и
|
||||||
| Неизвестная ошибка | Безопасная ошибка API | Повторить загрузку |
|
metadata обозначает initial loading либо временную недоступность, без нулевых
|
||||||
|
итогов. `503` означает ошибку обслуживания самого HTTP-запроса, а не
|
||||||
|
завершившуюся фоновую работу.
|
||||||
|
|
||||||
`INVALID_TBANK_TOKEN` и `404` не заменяются серверным запасным снимком.
|
Денежные правила находятся в [справочнике](../data-reference.md), проверка
|
||||||
Временная ошибка определения доступных счетов также может использовать
|
расхождения — в [диагностике](../how-to/investigate-discrepancy.md), RPC/лимиты
|
||||||
пригодный снимок того же поколения. При временном сбое метаданных сначала
|
— в [интеграции T-Bank](../../../integrations/tbank.md).
|
||||||
используются ранее успешные сведения, а при их отсутствии — идентификатор
|
|
||||||
инструмента; это не устанавливает `isStale`. Отказ авторизации при поиске
|
|
||||||
метаданных не маскируется запасным названием.
|
|
||||||
|
|
||||||
Для запроса самого портфеля на клиенте нет polling, автоматического повтора или
|
|
||||||
обновления при возврате фокуса. При обновлении того же уже показанного
|
|
||||||
результата клиент оставляет его на экране, добавляя индикатор загрузки
|
|
||||||
или предупреждение с доступной с клавиатуры повторной попыткой.
|
|
||||||
Это клиентский сохранённый ответ, независимо от серверного `isStale`.
|
|
||||||
|
|
||||||
Frontend отображает название и остальные сведения из ответа портфеля.
|
|
||||||
Повторные попытки поиска для акций, облигаций и других типов инструментов
|
|
||||||
принадлежат [общему клиенту T-Bank на backend](../../../integrations/tbank.md#конфигурация-и-ограничения).
|
|
||||||
|
|
||||||
Первое обращение к крупному портфелю может потребовать много метаданных.
|
|
||||||
Переполнение общей очереди RPC может произойти при конкурирующих запросах;
|
|
||||||
поиск метаданных в этом случае повторяется с ограниченными паузами. После
|
|
||||||
успешного поиска метаданные используются повторно до истечения
|
|
||||||
кэша. Значение `TBANK_RATE_LIMIT_PER_SECOND` следует соотносить с квотой
|
|
||||||
источника; его увеличение не является гарантированным исправлением
|
|
||||||
ограничения самого T-Bank.
|
|
||||||
|
|
||||||
Проверяемые правила: [свежесть и запасной снимок](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md#requirement-свежесть-и-fallback-полного-portfolio-snapshot),
|
|
||||||
[клиентские состояния](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md#requirement-состояния-и-ошибки-portfolio-на-клиенте).
|
|
||||||
Для расследования конкретного значения используйте
|
|
||||||
[порядок диагностики](../how-to/investigate-discrepancy.md).
|
|
||||||
|
|||||||
@@ -12,12 +12,13 @@
|
|||||||
величины. Убедитесь, что сравниваемые значения относятся к одному составу
|
величины. Убедитесь, что сравниваемые значения относятся к одному составу
|
||||||
счетов и инструменту. Название или тикер сами по себе не доказывают
|
счетов и инструменту. Название или тикер сами по себе не доказывают
|
||||||
совпадение выпуска; используйте доступные UID, FIGI и ISIN.
|
совпадение выпуска; используйте доступные UID, FIGI и ISIN.
|
||||||
2. **Сопоставьте время.** Проверьте `lastSuccessfulAt`, `isStale` и наличие
|
2. **Сопоставьте время.** Проверьте `lastSuccessfulAt`, `isStale` и показанный
|
||||||
предупреждения о клиентском обновлении. Серверный снимок не обещает время
|
исход последней часовой попытки. Серверный снимок не обещает время
|
||||||
каждой котировки, а общий портфель может соединять разные моменты.
|
каждой котировки, а общий портфель может соединять разные моменты.
|
||||||
Если запрос завершился ошибкой, доступное действие «Повторить» должно
|
Действия «Повторить» на экране нет: временный сбой чтения восстанавливается
|
||||||
обновить результат или оставить объяснимую ошибку. Перезагрузка backend
|
автоматически, поэтому расхождение проверяйте по показанному состоянию
|
||||||
удаляет кэш и не восстанавливает историю стоимости.
|
источника, а не по результату ручного обновления. Перезагрузка backend
|
||||||
|
сохраняет последний полный снимок в SQLite, но не создаёт историю стоимости.
|
||||||
3. **Проверьте состав суммы.** Для одной валюты итог складывается из
|
3. **Проверьте состав суммы.** Для одной валюты итог складывается из
|
||||||
вычислимых стоимостей позиций и доступных денег. Не добавляйте деньги
|
вычислимых стоимостей позиций и доступных денег. Не добавляйте деньги
|
||||||
второй раз, не складывайте валюты и не включайте заблокированные средства.
|
второй раз, не складывайте валюты и не включайте заблокированные средства.
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Результат инвестирования
|
||||||
|
|
||||||
|
Вкладка «Результат» на странице брокерского счёта и в режиме
|
||||||
|
[«Все счета»](all-accounts.md) отвечает на два вопроса:
|
||||||
|
|
||||||
|
- сколько заработали вложенные деньги;
|
||||||
|
- обогнал ли пользователь рынок.
|
||||||
|
|
||||||
|
Результат считается за всё время истории счёта, только в рублях. Решение и
|
||||||
|
отклонённые альтернативы описаны в
|
||||||
|
[ADR 023](../../adr/023-investment-performance-vs-index.md).
|
||||||
|
|
||||||
|
## Показатели
|
||||||
|
|
||||||
|
| Показатель | Смысл |
|
||||||
|
| --- | --- |
|
||||||
|
| Вложено (нетто) | Сумма пополнений минус сумма выводов рублей |
|
||||||
|
| Прибыль | Текущая стоимость портфеля минус чистые вложения |
|
||||||
|
| Доходность | Ставка XIRR с учётом сумм и дат пополнений и выводов |
|
||||||
|
| Те же деньги в индексе Мосбиржи | Стоимость и доходность тех же пополнений и выводов, вложенных в индекс полной доходности MCFTRR (с дивидендами после налогов) |
|
||||||
|
| Опережение или отставание рынка | Разница доходностей в процентных пунктах и разница стоимостей в рублях |
|
||||||
|
|
||||||
|
Ценовой индекс IMOEX не учитывает дивиденды. Он показан на графике только
|
||||||
|
для справки и в сравнении не участвует.
|
||||||
|
|
||||||
|
Период начинается с первого пополнения, а не с открытия счёта. Если деньги
|
||||||
|
вложены меньше 365 дней, проценты показаны за фактическое число дней и не
|
||||||
|
пересчитываются в годовые.
|
||||||
|
|
||||||
|
## Какие операции считаются потоками
|
||||||
|
|
||||||
|
Внешние потоки — только пополнения и выводы рублей (типы T-Bank `INPUT`,
|
||||||
|
`INPUT_ACQUIRING`, `INPUT_SWIFT`, `INP_MULTI` и соответствующие `OUTPUT`).
|
||||||
|
Сделки, купоны, дивиденды, погашения, налоги и комиссии уже отражены в
|
||||||
|
текущей стоимости и потоками не являются. Учитываются только операции до
|
||||||
|
момента последней полной оценки портфеля.
|
||||||
|
|
||||||
|
## Когда результата нет
|
||||||
|
|
||||||
|
Число не подменяется нулём: вместо него показывается причина.
|
||||||
|
|
||||||
|
| Причина | Когда возникает |
|
||||||
|
| --- | --- |
|
||||||
|
| `HISTORY_START_UNKNOWN` | Неизвестна дата открытия счёта |
|
||||||
|
| `OPERATIONS_HISTORY_INCOMPLETE` | История операций не получена полностью до момента оценки |
|
||||||
|
| `PORTFOLIO_VALUATION_INCOMPLETE` | Нет полной оценки портфеля |
|
||||||
|
| `NON_RUB_ACTIVITY` | Есть потоки или остатки не в рублях |
|
||||||
|
| `UNVALUED_SECURITIES_TRANSFER` | Бумаги введены или выведены без денежной оценки |
|
||||||
|
| `UNCONFIRMED_TRANSFER` | В «Все счета» есть похожие на перевод между своими счетами операции, связь которых источник не подтверждает |
|
||||||
|
| `ACCOUNT_VALUATION_DATES_DIFFER` | В «Все счета» последние оценки счетов сделаны в разные московские дни |
|
||||||
|
| `NO_SOLUTION` | Уравнение доходности не имеет решения; прибыль остаётся |
|
||||||
|
| `AMBIGUOUS_RETURN` | Потоки меняют знак больше одного раза, и единственность ставки не доказана; прибыль остаётся |
|
||||||
|
| `INDEX_HISTORY_UNAVAILABLE` | Нет истории индекса за период; прибыль и доходность остаются, сравнения нет |
|
||||||
|
|
||||||
|
В «Все счета» счёт с причиной исключается, а результат помечается
|
||||||
|
неполным со списком исключённых счетов. Если считать не по чему, результат
|
||||||
|
недоступен целиком.
|
||||||
|
|
||||||
|
## График
|
||||||
|
|
||||||
|
На графике показаны ступенчатая линия чистых вложений, стоимость тех же
|
||||||
|
денег в индексе по закрытиям торговых дней, справочная линия индекса без
|
||||||
|
дивидендов и отметка текущей стоимости портфеля на конец периода. Линии
|
||||||
|
прошлой стоимости портфеля нет: такая история пока не хранится.
|
||||||
|
|
||||||
|
Нормативное поведение задаёт change
|
||||||
|
`2026-11-07-investment-performance-vs-index` в репозитории specs, форму
|
||||||
|
ответа — [HTTP API](../../architecture/http-api.md#investment-performance).
|
||||||
@@ -17,11 +17,11 @@
|
|||||||
[справочнике данных](data-reference.md).
|
[справочнике данных](data-reference.md).
|
||||||
|
|
||||||
Доступность счёта определяется [текущим подключением и списком счетов](../accounts/overview.md).
|
Доступность счёта определяется [текущим подключением и списком счетов](../accounts/overview.md).
|
||||||
SQLite не хранит сам портфель: backend получает данные через серверную
|
Backend получает данные через серверную
|
||||||
[интеграцию T-Bank](../../integrations/tbank.md) и временно сохраняет успешный
|
[интеграцию T-Bank](../../integrations/tbank.md) и постоянно сохраняет последний
|
||||||
снимок в памяти. Frontend получает нормализованный результат через HTTP API.
|
полный снимок в SQLite. Frontend получает нормализованный результат через HTTP API.
|
||||||
[Поток загрузки](flows/portfolio-loading.md) объясняет границы доступа,
|
[Поток загрузки](flows/portfolio-loading.md) объясняет границы доступа,
|
||||||
свежий кэш и восстановление после временной ошибки.
|
свежесть снимка и восстановление после временной ошибки.
|
||||||
|
|
||||||
Обзор счёта также показывает пять последних [операций](../operations/overview.md),
|
Обзор счёта также показывает пять последних [операций](../operations/overview.md),
|
||||||
[полученный доход](../income/overview.md) текущего месяца и до пяти
|
[полученный доход](../income/overview.md) текущего месяца и до пяти
|
||||||
@@ -29,6 +29,8 @@ SQLite не хранит сам портфель: backend получает да
|
|||||||
самостоятельно: его ошибка или отсутствие данных не скрывает основной
|
самостоятельно: его ошибка или отсутствие данных не скрывает основной
|
||||||
портфель. Полученные выплаты и прогноз не добавляются к текущей стоимости
|
портфель. Полученные выплаты и прогноз не добавляются к текущей стоимости
|
||||||
повторно и не превращают результат открытой позиции в полную доходность.
|
повторно и не превращают результат открытой позиции в полную доходность.
|
||||||
|
Полную доходность с учётом пополнений и выводов показывает вкладка
|
||||||
|
[«Результат»](investment-performance.md).
|
||||||
|
|
||||||
## Как читать экран
|
## Как читать экран
|
||||||
|
|
||||||
@@ -64,6 +66,10 @@ SQLite не хранит сам портфель: backend получает да
|
|||||||
без логотипа и с подписью результата. Отсутствующие показатели скрываются,
|
без логотипа и с подписью результата. Отсутствующие показатели скрываются,
|
||||||
а не подменяются нулём или заглушкой.
|
а не подменяются нулём или заглушкой.
|
||||||
|
|
||||||
|
Рядом с вкладкой «Позиции» на странице отдельного счёта есть вкладка
|
||||||
|
[«Концентрация»](concentration.md): она показывает, где сконцентрированы
|
||||||
|
деньги — крупнейшие бумаги, секторы и эмитенты. В режиме «Все счета» её нет.
|
||||||
|
|
||||||
Существует расхождение: [согласованная спецификация представления позиций](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md#requirement-представление-карточек-позиций-на-клиенте)
|
Существует расхождение: [согласованная спецификация представления позиций](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md#requirement-представление-карточек-позиций-на-клиенте)
|
||||||
требует доступности всех иных типов в отдельной секции, а текущие frontend
|
требует доступности всех иных типов в отдельной секции, а текущие frontend
|
||||||
и его проверка исключают `CURRENCY` только из списка карточек. Такие позиции
|
и его проверка исключают `CURRENCY` только из списка карточек. Такие позиции
|
||||||
@@ -79,18 +85,23 @@ SQLite не хранит сам портфель: backend получает да
|
|||||||
не содержат данных и не получают фокус. При обычном фоновом обновлении
|
не содержат данных и не получают фокус. При обычном фоновом обновлении
|
||||||
сохраняются предыдущие суммы и время успешного получения в UTC.
|
сохраняются предыдущие суммы и время успешного получения в UTC.
|
||||||
|
|
||||||
Серверный `isStale` обозначает разрешённый запасной снимок backend.
|
Серверный `isStale` обозначает возраст снимка старше часа: обновлением
|
||||||
Клиентское предупреждение означает неудачную попытку обновить уже показанный
|
владеет часовой обход, а не запрос экрана. Он показывается рядом с суммами
|
||||||
ответ. Эти состояния независимы и могут появиться одновременно.
|
вместе с временем последнего успешного результата и исходом последней часовой
|
||||||
Успешный повторный запрос обновляет суммы и время, снимает клиентскую ошибку;
|
попытки. Неудачная попытка клиента обновить уже показанный ответ о себе не
|
||||||
серверное предупреждение определяется новым `isStale`.
|
сообщает: эти состояния независимы, но пользователю видимо только состояние
|
||||||
|
источника. Успешное автоматическое перечитывание обновляет суммы и время;
|
||||||
|
обозначение устаревания определяется новым `isStale`.
|
||||||
Правила времени и ошибок приведены в [потоке загрузки](flows/portfolio-loading.md).
|
Правила времени и ошибок приведены в [потоке загрузки](flows/portfolio-loading.md).
|
||||||
|
|
||||||
В текущем решении нет долговременной истории снимков, графиков стоимости
|
Отдельного чтения истории стоимости нет, но каждый полный снимок
|
||||||
портфеля, валютной конвертации, учёта виртуальных позиций или заблокированных
|
сохраняет точку оценки по счёту и валюте для будущих поддерживаемых
|
||||||
средств. Кэш теряется при перезапуске и рассчитан на локальную работу одного
|
представлений; публичного графика в текущем решении нет. Нет также
|
||||||
экземпляра backend. Отдельной команды принудительного обновления на экране
|
валютной конвертации, учёта виртуальных позиций или заблокированных
|
||||||
нет; повторная попытка доступна после ошибки.
|
средств. Последний полный снимок сохраняется после перезапуска;
|
||||||
|
обновлением личных данных владеет часовой обход, рассчитанный на один
|
||||||
|
экземпляр backend. Действие «Обновить» выполняет только локальное
|
||||||
|
перечитывание открытого экрана и не запускает работу с T-Bank.
|
||||||
|
|
||||||
Для проверки конкретной суммы используйте
|
Для проверки конкретной суммы используйте
|
||||||
[руководство по расхождениям](how-to/investigate-discrepancy.md).
|
[руководство по расхождениям](how-to/investigate-discrepancy.md).
|
||||||
|
|||||||
+126
-6
@@ -4,6 +4,14 @@
|
|||||||
Связи между ними объясняет [обзор продукта](overview.md), подробные правила —
|
Связи между ними объясняет [обзор продукта](overview.md), подробные правила —
|
||||||
[предметные области](README.md#предметные-области).
|
[предметные области](README.md#предметные-области).
|
||||||
|
|
||||||
|
## Релиз развёртывания WealthLedger
|
||||||
|
|
||||||
|
Версия комплекта установки WealthLedger, закрепляющая проверенную совместимую
|
||||||
|
пару релизов frontend и backend. Понятие принято в
|
||||||
|
[ADR 016](adr/016-versioned-deployment-bundle.md).
|
||||||
|
|
||||||
|
_Не использовать_: релиз frontend или backend как синоним релиза всей установки.
|
||||||
|
|
||||||
## User
|
## User
|
||||||
|
|
||||||
Локальный владелец данных WealthLedger, идентифицируемый `id` и
|
Локальный владелец данных WealthLedger, идентифицируемый `id` и
|
||||||
@@ -111,6 +119,46 @@ Instrument. Она остаётся в `positions` и не объединяет
|
|||||||
|
|
||||||
_Не использовать_: денежный остаток, cash balance.
|
_Не использовать_: денежный остаток, cash balance.
|
||||||
|
|
||||||
|
## Концентрация
|
||||||
|
|
||||||
|
Вкладка страницы одного Brokerage Account, показывающая, какие акции,
|
||||||
|
облигации, секторы и эмитенты занимают наибольшую часть текущей оценки
|
||||||
|
Portfolio, без валютной конвертации. Отличается от [структуры
|
||||||
|
портфеля](domains/portfolio/data-reference.md#структура-и-доли): в её разрезах
|
||||||
|
деньги не входят в знаменатель долей.
|
||||||
|
|
||||||
|
_Не использовать_: структура портфеля, диверсификация.
|
||||||
|
|
||||||
|
## Разрез
|
||||||
|
|
||||||
|
Одна из четырёх группировок вкладки «Концентрация»: «Акции», «Облигации»,
|
||||||
|
«Секторы», «Эмитенты». Элемент разреза — единица ранжирования; сектор
|
||||||
|
агрегирует позиции `STOCK`, `BOND` и `FUND`, эмитенты — `STOCK` и `BOND`.
|
||||||
|
|
||||||
|
_Не использовать_: фильтр, категория структуры.
|
||||||
|
|
||||||
|
## Сектор
|
||||||
|
|
||||||
|
Значение сектора инструмента в том виде, как его определяет T-Bank, без
|
||||||
|
перекодирования и собственной классификации WealthLedger. Заполняется для
|
||||||
|
позиций `STOCK`, `BOND` и `FUND`; государственные облигации определяются
|
||||||
|
значением `government` с подписью «Государственный сектор». Позиция без
|
||||||
|
сектора попадает в явную группу «Сектор не указан».
|
||||||
|
|
||||||
|
_Не использовать_: отрасль, собственная классификация приложения.
|
||||||
|
|
||||||
|
## Эмитент
|
||||||
|
|
||||||
|
В разрезе «Эмитенты» вкладки «Концентрация» — бренд актива по данным T-Bank,
|
||||||
|
объединяющий акции и облигации одной компании; фонды в разрез не входят.
|
||||||
|
Название эмитента — название бренда. **Оговорка:** в этом разрезе слово
|
||||||
|
означает бренд актива T-Bank, а не юридического эмитента ценной бумаги;
|
||||||
|
[рейтинг выпуска](integrations/nkr.md) не подставляется вместо рейтинга
|
||||||
|
эмитента и наоборот. Позиция без бренда попадает в явную группу
|
||||||
|
«Эмитент не указан».
|
||||||
|
|
||||||
|
_Не использовать_: юридический эмитент, эмитент рейтинга.
|
||||||
|
|
||||||
## Instrument
|
## Instrument
|
||||||
|
|
||||||
Ценная бумага или иной торгуемый актив T-Bank, на который ссылается Portfolio
|
Ценная бумага или иной торгуемый актив T-Bank, на который ссылается Portfolio
|
||||||
@@ -120,6 +168,14 @@ Instrument обогащают отображение Position или Event, но
|
|||||||
|
|
||||||
_Не использовать_: бумага, тикер, актив позиции.
|
_Не использовать_: бумага, тикер, актив позиции.
|
||||||
|
|
||||||
|
## Скринер акций
|
||||||
|
|
||||||
|
Подбор выпусков акций по заданным условиям из каталога T-Bank с указанием
|
||||||
|
времени получения списка. Скринер отличается от текстового поиска конкретного
|
||||||
|
инструмента, личного портфеля и подтверждения исполнимости сделки.
|
||||||
|
|
||||||
|
_Не использовать_: гарантия покупки, личная проверка доступа к сделке.
|
||||||
|
|
||||||
## Instrument Logo
|
## Instrument Logo
|
||||||
|
|
||||||
Имя файла бренда T-Bank для отображения Instrument в интерфейсе;
|
Имя файла бренда T-Bank для отображения Instrument в интерфейсе;
|
||||||
@@ -144,6 +200,16 @@ _Не использовать_: валютная позиция, заблоки
|
|||||||
|
|
||||||
_Не использовать_: история портфеля, snapshot для графика.
|
_Не использовать_: история портфеля, snapshot для графика.
|
||||||
|
|
||||||
|
## Точка истории стоимости портфеля
|
||||||
|
|
||||||
|
Полная оценка одного Brokerage Account в одной валюте на момент успешного
|
||||||
|
обновления Portfolio; последовательность таких точек описывает изменение
|
||||||
|
оценки во времени, включая неторговые дни. Не является Portfolio Snapshot
|
||||||
|
или [Результатом инвестирования](#результат-инвестирования); отсутствие цены
|
||||||
|
позиции не приравнивается к нулю.
|
||||||
|
|
||||||
|
_Не использовать_: снимок состава портфеля.
|
||||||
|
|
||||||
## Client Cached Snapshot
|
## Client Cached Snapshot
|
||||||
|
|
||||||
Технический термин: последний успешно полученный frontend-ответ React Query, который остаётся
|
Технический термин: последний успешно полученный frontend-ответ React Query, который остаётся
|
||||||
@@ -169,6 +235,19 @@ _Не использовать_: история Portfolio, баланс счёт
|
|||||||
|
|
||||||
_Не использовать_: проводка, рассчитанная операция.
|
_Не использовать_: проводка, рассчитанная операция.
|
||||||
|
|
||||||
|
## Полнота истории счёта
|
||||||
|
|
||||||
|
Подтверждённость того, что за указанный период загружены все доступные
|
||||||
|
исполненные операции счёта на момент успешной сверки с брокером. Полный период
|
||||||
|
может не содержать операций; полнота не гарантирует отсутствия последующих
|
||||||
|
исправлений брокера и отличается от свежести данных.
|
||||||
|
|
||||||
|
Понятие принято в [ADR 017](adr/017-local-tbank-reads-and-background-sync.md).
|
||||||
|
Текущее хранение coverage и независимая свежесть объяснены в
|
||||||
|
[модели данных](architecture/cross-cutting/data-freshness.md).
|
||||||
|
|
||||||
|
_Не использовать_: свежесть истории, отсутствие операций как признак полноты.
|
||||||
|
|
||||||
## Operation Category
|
## Operation Category
|
||||||
|
|
||||||
Нормализованная категория Ledger Entry для фильтрации и отображения:
|
Нормализованная категория Ledger Entry для фильтрации и отображения:
|
||||||
@@ -178,13 +257,15 @@ _Не использовать_: upstream operation type как category.
|
|||||||
|
|
||||||
## Ledger Page Cache
|
## Ledger Page Cache
|
||||||
|
|
||||||
Технический термин: кэш страницы Account Ledger, отделённый от кэша отдельных операций.
|
Исторический технический термин: RAM-кэш страницы Account Ledger.
|
||||||
|
Текущее чтение использует сохранённые операции и coverage в SQLite;
|
||||||
|
страница не является единицей полноты.
|
||||||
|
|
||||||
_Не использовать_: operations cache, кэш отдельных операций.
|
_Не использовать_: operations cache, кэш отдельных операций.
|
||||||
|
|
||||||
## Ledger Page Cache Key
|
## Ledger Page Cache Key
|
||||||
|
|
||||||
Ключ, разделяющий страницы журнала по пользователю, текущему подключению,
|
Исторический ключ RAM-кэша, разделявший страницы по пользователю, подключению,
|
||||||
счёту и параметрам запроса, включая необязательный UID инструмента.
|
счёту и параметрам запроса, включая необязательный UID инструмента.
|
||||||
Полный состав и правила нормализации описаны в
|
Полный состав и правила нормализации описаны в
|
||||||
[журнале операций](domains/operations/overview.md#получение-и-полнота).
|
[журнале операций](domains/operations/overview.md#получение-и-полнота).
|
||||||
@@ -231,8 +312,47 @@ _Не использовать_: upstream dividend type, MOEX row name.
|
|||||||
Результат анализа исполненных купонов и дивидендов по счёту за календарный
|
Результат анализа исполненных купонов и дивидендов по счёту за календарный
|
||||||
период включительно в часовом поясе Europe/Moscow. Сохраняет исходные валюты
|
период включительно в часовом поясе Europe/Moscow. Сохраняет исходные валюты
|
||||||
и знаки денежных выплат, переданных брокером. Это не налоговый расчёт,
|
и знаки денежных выплат, переданных брокером. Это не налоговый расчёт,
|
||||||
прогноз или инвестиционная доходность; правила расчёта объясняет
|
прогноз или [Результат инвестирования](#результат-инвестирования); правила
|
||||||
[область дохода](domains/income/overview.md).
|
расчёта объясняет [область дохода](domains/income/overview.md).
|
||||||
|
|
||||||
_Не использовать_: доходность, общий доход в одной валюте, гарантированный
|
_Не использовать_: доходность выплат, общий доход в одной валюте,
|
||||||
доход.
|
гарантированный доход.
|
||||||
|
|
||||||
|
## Результат инвестирования
|
||||||
|
|
||||||
|
Прибыль, доходность и сравнение с индексом Мосбиржи для счёта или «Все
|
||||||
|
счета» за всё время в рублях. Правила описаны в
|
||||||
|
[результате инвестирования](domains/portfolio/investment-performance.md),
|
||||||
|
решение — в [ADR 023](adr/023-investment-performance-vs-index.md).
|
||||||
|
|
||||||
|
_Не использовать_: нереализованный результат, доход.
|
||||||
|
|
||||||
|
## Внешний денежный поток
|
||||||
|
|
||||||
|
Пополнение (положительный поток) или вывод (отрицательный поток) рублей со
|
||||||
|
счёта. Сделки, выплаты, налоги и комиссии внешними потоками не являются.
|
||||||
|
|
||||||
|
_Не использовать_: операция, выплата.
|
||||||
|
|
||||||
|
## Доходность (XIRR)
|
||||||
|
|
||||||
|
Годовая ставка, при которой дисконтированные по фактическим дням внешние
|
||||||
|
потоки и текущая стоимость в сумме дают ноль. При истории короче года
|
||||||
|
показывается доходность за период. Единственное значение термина
|
||||||
|
«доходность» для личного результата.
|
||||||
|
|
||||||
|
_Не использовать_: доходность к погашению, рост цены позиции.
|
||||||
|
|
||||||
|
## Те же деньги в индексе Мосбиржи
|
||||||
|
|
||||||
|
Стоимость и доходность тех же внешних потоков, вложенных в индекс полной
|
||||||
|
доходности MCFTRR по закрытию дня потока. Ценовой IMOEX — только справка.
|
||||||
|
|
||||||
|
_Не использовать_: бенчмарк IMOEX, теневой портфель (в интерфейсе).
|
||||||
|
|
||||||
|
## Опережение рынка
|
||||||
|
|
||||||
|
Разница доходности пользователя и тех же денег в индексе в процентных пунктах
|
||||||
|
и разница стоимостей в рублях; отрицательное значение называется отставанием.
|
||||||
|
|
||||||
|
_Не использовать_: альфа.
|
||||||
|
|||||||
+28
-1
@@ -1,6 +1,7 @@
|
|||||||
# Интеграция с MOEX ISS
|
# Интеграция с MOEX ISS
|
||||||
|
|
||||||
MOEX ISS дополняет данные T-Bank публичной информацией об облигациях.
|
MOEX ISS дополняет данные T-Bank публичной информацией об облигациях и
|
||||||
|
дневными закрытиями индексов Мосбиржи.
|
||||||
Backend получает её по HTTPS в JSON с `iss.moex.com`; запросы содержат
|
Backend получает её по HTTPS в JSON с `iss.moex.com`; запросы содержат
|
||||||
идентификаторы выпуска, без token T-Bank и идентификатора брокерского счёта.
|
идентификаторы выпуска, без token T-Bank и идентификатора брокерского счёта.
|
||||||
MOEX не определяет доступ пользователя к инвестициям и не подтверждает
|
MOEX не определяет доступ пользователя к инвестициям и не подтверждает
|
||||||
@@ -47,6 +48,32 @@ HTTP-запрос ограничен таймаутом. Сеть, неуспе
|
|||||||
[странице облигации](../domains/instruments/bonds.md#получение-данных-и-восстановление).
|
[странице облигации](../domains/instruments/bonds.md#получение-данных-и-восстановление).
|
||||||
Наличие успешного HTTP-ответа не гарантирует котировку без задержки.
|
Наличие успешного HTTP-ответа не гарантирует котировку без задержки.
|
||||||
|
|
||||||
|
## Индексы
|
||||||
|
|
||||||
|
Для [результата инвестирования](../domains/portfolio/investment-performance.md)
|
||||||
|
backend читает дневные закрытия MCFTRR (полная доходность после налогов) и
|
||||||
|
IMOEX (ценовой, справка) из
|
||||||
|
`/iss/history/engines/stock/markets/index/securities/{SECID}.json` с
|
||||||
|
`history.columns=TRADEDATE,CLOSE`, постранично по `history.cursor`.
|
||||||
|
Любая некорректная страница считается сбоем: частичная история не
|
||||||
|
сохраняется.
|
||||||
|
|
||||||
|
Закрытия и подтверждённый диапазон покрытия хранятся локально в SQLite
|
||||||
|
(исключение из ADR 019, см. [ADR 023](../adr/023-investment-performance-vs-index.md)).
|
||||||
|
Догрузка выполняется при чтении результата одной одновременной загрузкой на
|
||||||
|
индекс: расширение диапазона назад — сразу, проверка ещё не опубликованного
|
||||||
|
закрытия и повтор после сбоя — не чаще раза в час. Недоступность
|
||||||
|
MOEX не возвращается как HTTP-ошибка: при достаточной сохранённой истории
|
||||||
|
результат считается по ней, иначе сравнение помечается
|
||||||
|
`INDEX_HISTORY_UNAVAILABLE`. События лога —
|
||||||
|
`integration.moex.index_history.succeeded` и `.failed`, без данных
|
||||||
|
пользователя.
|
||||||
|
|
||||||
|
ISS бесплатно отдаёт данные для ознакомления; приложение личное и не
|
||||||
|
распространяет их, источник указан в пояснении «Как считали».
|
||||||
|
|
||||||
|
## Ссылки
|
||||||
|
|
||||||
Условия обработки источника в прогнозе закреплены в
|
Условия обработки источника в прогнозе закреплены в
|
||||||
[спецификации будущих событий](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-upcoming-events/spec.md#requirement-upcoming-events-use-complete-source-results).
|
[спецификации будущих событий](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-upcoming-events/spec.md#requirement-upcoming-events-use-complete-source-results).
|
||||||
Точный механизм транспорта, разбора и сопоставления реализован в
|
Точный механизм транспорта, разбора и сопоставления реализован в
|
||||||
|
|||||||
+33
-13
@@ -14,12 +14,27 @@ WealthLedger обращается к Invest API на сервере по gRPC ov
|
|||||||
| Портфель и денежные остатки | Показать текущий состав инвестиций и итоги по исходным валютам |
|
| Портфель и денежные остатки | Показать текущий состав инвестиций и итоги по исходным валютам |
|
||||||
| Исполненные операции | Построить журнал и выделить полученные купоны и дивиденды |
|
| Исполненные операции | Построить журнал и выделить полученные купоны и дивиденды |
|
||||||
| Инструменты и рынок | Имена, идентификаторы, цены, свечи, параметры акций и облигаций |
|
| Инструменты и рынок | Имена, идентификаторы, цены, свечи, параметры акций и облигаций |
|
||||||
|
| Классификация инструментов | Сектор акций, облигаций и фондов и бренд актива для разрезов [концентрации](../domains/portfolio/concentration.md) |
|
||||||
| Купоны и дивиденды | Построить будущие события по текущим позициям и календарь инструмента |
|
| Купоны и дивиденды | Построить будущие события по текущим позициям и календарь инструмента |
|
||||||
|
|
||||||
|
Сектор получается из описаний инструмента (отдельные описания для акций,
|
||||||
|
облигаций и фондов, поиск по UID, затем по FIGI); эмитент — из описания актива
|
||||||
|
по `asset_uid` инструмента: его uid и название бренда. Эмитент не выводится из
|
||||||
|
названия, тикера или ISIN. Классификация хранится как последнее известное
|
||||||
|
значение с обновлением не чаще раза в сутки ([ADR
|
||||||
|
022](../adr/022-stored-instrument-classification.md)); все вызовы проходят
|
||||||
|
через общий лимитер запросов T-Bank.
|
||||||
|
|
||||||
T-Bank определяет исходные данные и права token. Backend проверяет локальную
|
T-Bank определяет исходные данные и права token. Backend проверяет локальную
|
||||||
сессию, выбирает подключение её пользователя и устанавливает доступ к счёту.
|
сессию, выбирает подключение её пользователя и устанавливает доступ к счёту.
|
||||||
Наличие идентификатора счёта в URL этого доступа не подтверждает.
|
Наличие идентификатора счёта в URL этого доступа не подтверждает.
|
||||||
|
|
||||||
|
Скринер акций получает полный online-каталог акций T-Bank для каждого нового
|
||||||
|
поиска. В него входят только инструменты, для которых каталог подтверждает и
|
||||||
|
доступность покупки, и торговли через API. Эти признаки описывают полученный
|
||||||
|
снимок каталога: они не подтверждают личные права пользователя, состояние торгов
|
||||||
|
или исполнение будущей заявки. Скринер не использует личный портфель как каталог.
|
||||||
|
|
||||||
[MOEX](moex.md) и [НКР](nkr.md) дополняют публичные сведения об облигациях; они не заменяют
|
[MOEX](moex.md) и [НКР](nkr.md) дополняют публичные сведения об облигациях; они не заменяют
|
||||||
брокера как источник личных позиций и исполненных выплат. Границы источников
|
брокера как источник личных позиций и исполненных выплат. Границы источников
|
||||||
показаны в [контексте](../architecture/context.md), смысл дополнений — в
|
показаны в [контексте](../architecture/context.md), смысл дополнений — в
|
||||||
@@ -35,7 +50,9 @@ Backend передаёт имя файла `logoName` из метаданных
|
|||||||
|
|
||||||
Логотипы используются в портфеле, операциях, будущих событиях, поиске и
|
Логотипы используются в портфеле, операциях, будущих событиях, поиске и
|
||||||
страницах инструментов. Отсутствующее имя или ошибка изображения оставляют
|
страницах инструментов. Отсутствующее имя или ошибка изображения оставляют
|
||||||
буквенную заглушку и не меняют финансовые значения. Правило построения URL
|
буквенную заглушку и не меняют финансовые значения. Логотип — это metadata
|
||||||
|
инструмента; он не равен эмитенту из [концентрации](../domains/portfolio/concentration.md),
|
||||||
|
который берётся из бренда актива. Правило построения URL
|
||||||
принадлежит [форматированию frontend](https://git.personal.ksv741.ru/WealthLedger/frontend/src/commit/9f7e342333b80bfacfd5bc394a2dcd0613d33ff3/src/shared/lib/display.ts).
|
принадлежит [форматированию frontend](https://git.personal.ksv741.ru/WealthLedger/frontend/src/commit/9f7e342333b80bfacfd5bc394a2dcd0613d33ff3/src/shared/lib/display.ts).
|
||||||
|
|
||||||
## Проверка и сохранение подключения
|
## Проверка и сохранение подключения
|
||||||
@@ -62,7 +79,7 @@ sequenceDiagram
|
|||||||
B->>T: GetAccounts OPEN по gRPC over TLS
|
B->>T: GetAccounts OPEN по gRPC over TLS
|
||||||
alt Проверка успешна, в том числе при пустом списке счетов
|
alt Проверка успешна, в том числе при пустом списке счетов
|
||||||
T-->>B: Доступные открытые счета
|
T-->>B: Доступные открытые счета
|
||||||
B->>D: Создать или заменить шифротекст AES-256-GCM
|
B->>D: Атомарно сохранить token, новую revision и membership
|
||||||
B-->>F: Подключено и время сохранения
|
B-->>F: Подключено и время сохранения
|
||||||
F->>F: Обновить статус и сбросить финансовые выборки
|
F->>F: Обновить статус и сбросить финансовые выборки
|
||||||
F-->>U: Показать подтверждённое подключение
|
F-->>U: Показать подтверждённое подключение
|
||||||
@@ -91,9 +108,15 @@ sequenceDiagram
|
|||||||
|
|
||||||
`INVALID_TBANK_TOKEN` означает отказ внешней аутентификации или прав доступа.
|
`INVALID_TBANK_TOKEN` означает отказ внешней аутентификации или прав доступа.
|
||||||
`TBANK_UNAVAILABLE` сообщает о временной или иной upstream-ошибке, включая
|
`TBANK_UNAVAILABLE` сообщает о временной или иной upstream-ошибке, включая
|
||||||
исчерпание очереди. Ответы и ошибки не содержат token. Удаление подключения
|
исчерпание очереди. Диагностические причины жизненного цикла часовой попытки
|
||||||
идемпотентно: удаляется локальный секрет и прекращается его использование
|
(`INTERRUPTED` — прерывание до нормального завершения, `DEADLINE` — истечение
|
||||||
для последующих запросов. Оно не описывает отзыв token на стороне T-Bank.
|
ограничения ожидания шага) не относятся к ошибкам T-Bank: они остаются
|
||||||
|
внутренней диагностикой и не раскрываются в hourly-read. Истечение ограничения
|
||||||
|
ожидания не отменяет уже начатые обращения к T-Bank; их поздние результаты
|
||||||
|
остаются в строках шагов. Ответы и ошибки не содержат token. Удаление подключения
|
||||||
|
идемпотентно: удаляются локальный секрет, личные datasets и work; поздняя
|
||||||
|
публикация прежней revision запрещена. Общие неперсональные источники могут
|
||||||
|
сохраняться. Оно не описывает отзыв token на стороне T-Bank.
|
||||||
|
|
||||||
Согласованное поведение сохранено в workspace-спецификации
|
Согласованное поведение сохранено в workspace-спецификации
|
||||||
[T-Bank connection](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/tbank-connection/spec.md).
|
[T-Bank connection](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/tbank-connection/spec.md).
|
||||||
@@ -106,7 +129,7 @@ sequenceDiagram
|
|||||||
использования финансовых данных. Это относится и к сохранению того же самого
|
использования финансовых данных. Это относится и к сохранению того же самого
|
||||||
token: предыдущее подключение не становится источником данных нового.
|
token: предыдущее подключение не становится источником данных нового.
|
||||||
Неуспешная операция сохраняет прежнее подключение и его доступные данные.
|
Неуспешная операция сохраняет прежнее подключение и его доступные данные.
|
||||||
Механизм generation, поздние ответы и сброс клиентских выборок объясняет
|
Механизм внутренней revision, поздние ответы и сброс клиентских выборок объясняет
|
||||||
[свежесть данных](../architecture/cross-cutting/data-freshness.md#смена-подключения).
|
[свежесть данных](../architecture/cross-cutting/data-freshness.md#смена-подключения).
|
||||||
|
|
||||||
## Конфигурация и ограничения
|
## Конфигурация и ограничения
|
||||||
@@ -129,15 +152,12 @@ Backend хранит token в шифротексте AES-256-GCM с новым n
|
|||||||
[validateEnv](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/config/env.validation.ts) (справочный источник).
|
[validateEnv](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/config/env.validation.ts) (справочный источник).
|
||||||
Общий FIFO-лимитер ограничивает поток RPC и не допускает более 50
|
Общий FIFO-лимитер ограничивает поток RPC и не допускает более 50
|
||||||
ожидающих. Для `UNAVAILABLE`, `DEADLINE_EXCEEDED` и `RESOURCE_EXHAUSTED`
|
ожидающих. Для `UNAVAILABLE`, `DEADLINE_EXCEEDED` и `RESOURCE_EXHAUSTED`
|
||||||
обычный RPC допускает один повтор через секунду. Deadline каждой попытки — 15 секунд,
|
каждый RPC допускает один повтор через секунду. Deadline каждой попытки — 15 секунд,
|
||||||
а ожидание места в лимитере не является временем этой сетевой попытки.
|
а ожидание места в лимитере не является временем этой сетевой попытки.
|
||||||
Для поиска метаданных `GetInstrumentBy` разрешены две повторные попытки
|
Метаданные используют ту же retry policy. Переполненная очередь завершается
|
||||||
с паузами 2 и 4 секунды после неудачных попыток. Они применяются к тем же
|
ошибкой сразу, без повторов admission. Постоянные ошибки и отказ авторизации
|
||||||
временным кодам и к переполнению локальной очереди; перед каждой попыткой
|
|
||||||
проверяется её вместимость. Постоянные ошибки и отказ авторизации
|
|
||||||
не повторяются. Подтверждённый `NOT_FOUND` при поиске метаданных означает
|
не повторяются. Подтверждённый `NOT_FOUND` при поиске метаданных означает
|
||||||
отсутствие инструмента, а не временный сбой. Остальные RPC при полной
|
отсутствие инструмента, а не временный сбой.
|
||||||
очереди завершаются сразу.
|
|
||||||
Параметры попыток принадлежат
|
Параметры попыток принадлежат
|
||||||
[T-Bank client](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/integrations/tbank/tbank.client.ts) (справочный источник);
|
[T-Bank client](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/integrations/tbank/tbank.client.ts) (справочный источник);
|
||||||
решение об ограниченных повторах — [ADR 005](../adr/005-tbank-upstream-resilience.md).
|
решение об ограниченных повторах — [ADR 005](../adr/005-tbank-upstream-resilience.md).
|
||||||
|
|||||||
+6
-3
@@ -20,11 +20,12 @@ WealthLedger — локальное приложение для частного
|
|||||||
| Найти свой инвестиционный счёт | [Список счетов](domains/accounts/overview.md), их типы и доступные сводки |
|
| Найти свой инвестиционный счёт | [Список счетов](domains/accounts/overview.md), их типы и доступные сводки |
|
||||||
| Вести банковские карты | [Ручной учёт](domains/cards/overview.md): независимые балансы, расходы, пополнения, переводы, возвраты и месячные сводки |
|
| Вести банковские карты | [Ручной учёт](domains/cards/overview.md): независимые балансы, расходы, пополнения, переводы, возвраты и месячные сводки |
|
||||||
| Понять текущий состав инвестиций | [Портфель](domains/portfolio/overview.md): позиции, денежные остатки и итоги по валютам |
|
| Понять текущий состав инвестиций | [Портфель](domains/portfolio/overview.md): позиции, денежные остатки и итоги по валютам |
|
||||||
|
| Увидеть, где сконцентрированы деньги | [Концентрация](domains/portfolio/concentration.md): крупнейшие бумаги, секторы и эмитенты счёта |
|
||||||
| Разобрать уже совершённые движения | [Журнал операций](domains/operations/overview.md): исполненные операции с фильтрами и последовательной загрузкой истории |
|
| Разобрать уже совершённые движения | [Журнал операций](domains/operations/overview.md): исполненные операции с фильтрами и последовательной загрузкой истории |
|
||||||
| Посчитать полученные выплаты | [Пассивный доход](domains/income/overview.md): исполненные купоны и дивиденды за календарный период |
|
| Посчитать полученные выплаты | [Пассивный доход](domains/income/overview.md): исполненные купоны и дивиденды за календарный период |
|
||||||
| Посмотреть ожидаемые события | [Будущие выплаты](domains/income/upcoming-events.md): купоны, дивиденды, погашения и оферты по текущим позициям |
|
| Посмотреть ожидаемые события | [Будущие выплаты](domains/income/upcoming-events.md): купоны, дивиденды, погашения и оферты по текущим позициям |
|
||||||
| Увидеть общий состав инвестиций | Режим [«Все счета»](domains/portfolio/all-accounts.md), сохраняющий принадлежность строк к счетам |
|
| Увидеть общий состав инвестиций | Режим [«Все счета»](domains/portfolio/all-accounts.md), сохраняющий принадлежность строк к счетам |
|
||||||
| Изучить конкретный инструмент | Поиск и страницы [акций](domains/instruments/stocks.md) и [облигаций](domains/instruments/bonds.md): рынок, личная позиция, история и события |
|
| Изучить конкретный инструмент | [Скринер акций](domains/instruments/stocks.md) по параметрам каталога, страницы акций и поиск [облигаций](domains/instruments/bonds.md): рынок, личная позиция, история и события |
|
||||||
|
|
||||||
## Как связаны представления
|
## Как связаны представления
|
||||||
|
|
||||||
@@ -40,8 +41,10 @@ WealthLedger — локальное приложение для частного
|
|||||||
|
|
||||||
Портфель отвечает на вопрос «что есть сейчас», журнал — «что исполнено»,
|
Портфель отвечает на вопрос «что есть сейчас», журнал — «что исполнено»,
|
||||||
аналитика дохода — «какие выплаты уже получены», будущие события — «что
|
аналитика дохода — «какие выплаты уже получены», будущие события — «что
|
||||||
ожидается». Прогноз не подтверждает фактическое зачисление. Полученные купоны
|
ожидается», [результат инвестирования](domains/portfolio/investment-performance.md) —
|
||||||
и дивиденды не равны инвестиционной доходности или результату после налогов.
|
«как сработали вложенные деньги и обогнал ли я индекс Мосбиржи». Прогноз не
|
||||||
|
подтверждает фактическое зачисление. Полученные купоны и дивиденды сами по
|
||||||
|
себе не равны доходности или результату после налогов.
|
||||||
|
|
||||||
Денежная сумма всегда относится к одной валюте. Недоступное значение
|
Денежная сумма всегда относится к одной валюте. Недоступное значение
|
||||||
отличается от нуля, а объединение счетов не означает автоматическую
|
отличается от нуля, а объединение счетов не означает автоматическую
|
||||||
|
|||||||
Generated
+431
-1070
File diff suppressed because it is too large
Load Diff
+22
-13
@@ -1,29 +1,38 @@
|
|||||||
{
|
{
|
||||||
"name": "wealth-ledger-docs",
|
"name": "wealth-ledger-docs",
|
||||||
"version": "0.1.1",
|
"version": "0.1.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "24.21.0"
|
"node": "24.22.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "markdownlint-cli2 \"**/*.md\" \"#node_modules\"",
|
"lint": "markdownlint --dot --rules .markdownlint-rule-relative-links.cjs --ignore node_modules --ignore CHANGELOG.md -- \"**/*.md\" && markdownlint --config .markdownlint-changelog.json --rules .markdownlint-rule-relative-links.cjs -- CHANGELOG.md",
|
||||||
"verify": "npm run lint && git diff --check",
|
"verify": "npm run lint && git diff --check",
|
||||||
"release:preview": "release-it --dry-run --ci",
|
"release:preview": "release-it --dry-run --ci",
|
||||||
"release:prepare": "release-it --ci",
|
"release:prepare": "release-it --ci",
|
||||||
"commitlint": "commitlint"
|
"commitlint": "commitlint"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "21.2.2",
|
"@commitlint/cli": "21.2.4",
|
||||||
"@commitlint/config-conventional": "21.2.2",
|
"@commitlint/config-conventional": "21.2.4",
|
||||||
"@release-it/conventional-changelog": "12.0.0",
|
"@release-it/conventional-changelog": "12.0.2",
|
||||||
"conventional-changelog-conventionalcommits": "10.4.0",
|
"conventional-changelog-conventionalcommits": "10.4.1",
|
||||||
"markdownlint-cli2": "0.23.2",
|
"markdownlint-cli": "0.49.1",
|
||||||
"markdownlint-rule-relative-links": "5.1.2",
|
"markdownlint-rule-relative-links": "5.1.4",
|
||||||
"release-it": "21.0.2"
|
"release-it": "21.1.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"markdownlint-cli2@0.23.2": {
|
"fast-uri": "3.1.9",
|
||||||
"smol-toml": "1.8.0"
|
"katex": "0.19.1",
|
||||||
}
|
"markdownlint-cli@0.49.1": {
|
||||||
|
"js-yaml": "5.4.2",
|
||||||
|
"minimatch": "11.0.2",
|
||||||
|
"smol-toml": "1.9.1"
|
||||||
|
},
|
||||||
|
"release-it": {
|
||||||
|
"basic-ftp": "6.2.3",
|
||||||
|
"undici": "7.30.1"
|
||||||
|
},
|
||||||
|
"markdown-it": "15.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-1
@@ -2,7 +2,8 @@
|
|||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": [
|
||||||
"config:recommended",
|
"config:recommended",
|
||||||
":semanticCommitTypeAll(chore)"
|
":semanticCommitTypeAll(chore)",
|
||||||
|
"security:minimumReleaseAgeNpm"
|
||||||
],
|
],
|
||||||
"timezone": "Europe/Moscow",
|
"timezone": "Europe/Moscow",
|
||||||
"labels": [
|
"labels": [
|
||||||
@@ -10,6 +11,13 @@
|
|||||||
],
|
],
|
||||||
"dependencyDashboard": true,
|
"dependencyDashboard": true,
|
||||||
"rangeStrategy": "pin",
|
"rangeStrategy": "pin",
|
||||||
|
"prConcurrentLimit": 5,
|
||||||
|
"lockFileMaintenance": {
|
||||||
|
"enabled": true,
|
||||||
|
"schedule": [
|
||||||
|
"before 4am on the first day of the month"
|
||||||
|
]
|
||||||
|
},
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": [
|
"matchUpdateTypes": [
|
||||||
@@ -18,6 +26,27 @@
|
|||||||
],
|
],
|
||||||
"groupName": "non-major dependencies"
|
"groupName": "non-major dependencies"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": [
|
||||||
|
"github-actions"
|
||||||
|
],
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"minor",
|
||||||
|
"patch"
|
||||||
|
],
|
||||||
|
"groupName": "non-major github actions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": [
|
||||||
|
"docker-compose",
|
||||||
|
"dockerfile"
|
||||||
|
],
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"minor",
|
||||||
|
"patch"
|
||||||
|
],
|
||||||
|
"groupName": "non-major container images"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": [
|
"matchUpdateTypes": [
|
||||||
"major"
|
"major"
|
||||||
|
|||||||
Reference in New Issue
Block a user