Кабинет: верная оценка валютных выпусков и читаемые числа

- prices.go: у выпуска берётся валюта номинала (FaceUnit); «замещающие» и еврооблигации
  котируются в процентах от валютного номинала, поэтому тело переводится в рубли по курсу,
  НКД экспорт отдаёт уже в рублях. Без курса оценка не выдумывается
- lkfx.go: официальные курсы валют из учётной системы (OD_VALUES_RATES, MARKET=0; DEPO,
  затем SPECTRE — берём свежее), кэш на час; дата курса показывается под таблицей
- БР-719 онлайн: было 17 млн ₽ (валютные считались рублёвыми), стало 522 млн ₽ — сходится
  с архивом брокерского учёта (НОВАТЭК 001Р-02: 162,65 млн против 162,32 млн на дату среза)
- вёрстка: отступ между числовыми колонками (числа больше не слипаются), больше воздуха
  между показателями в шапке, кнопка «Новое поручение» компактнее, цены от 10 — два знака
- структура портфеля: плитки с долей и суммой вместо строки в одну линию; блок денег
  показывается, только когда валют несколько

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
zuevav
2026-09-18 18:15:38 +03:00
parent e37d0839eb
commit 9712b4feda
5 changed files with 178 additions and 22 deletions
+29 -3
View File
@@ -11,6 +11,7 @@ import (
"context"
"fmt"
"sort"
"strconv"
"strings"
"time"
)
@@ -151,6 +152,7 @@ func lkOnlinePortfolio(ctx context.Context, rc *RuntimeConfig, contract string)
}
}
prices := lkPrices(ctx, rc, isins)
fxUsed := map[string]SecurityPrice{}
for _, r := range pos {
s := secByCode[r.code]
@@ -161,10 +163,19 @@ func lkOnlinePortfolio(ctx context.Context, rc *RuntimeConfig, contract string)
if px, ok := prices[s.isin]; ok && px.Price > 0 {
lp.Price, lp.InPct, lp.DayPct = px.Price, px.InPct, px.ChangePc
lp.Value = px.Value(r.qty)
out.Priced++
if r.avg > 0 {
if lp.Value > 0 {
out.Priced++
}
switch {
case lp.Value <= 0 && px.FaceCur != "":
lp.Note = "номинал в " + px.FaceCur + ": нет курса для оценки"
case px.FX > 0 && px.FX != 1:
lp.Note = "номинал в " + px.FaceCur
fxUsed[px.FaceCur] = px // курс покажем одной строкой под таблицей
}
if r.avg > 0 && lp.Value > 0 {
// средняя цена в тех же единицах, что и котировка: для облигаций — процент номинала
cost := SecurityPrice{Price: r.avg, Face: px.Face, InPct: px.InPct}
cost := SecurityPrice{Price: r.avg, Face: px.Face, InPct: px.InPct, FaceCur: px.FaceCur, FX: px.FX}
lp.Cost = cost.Value(r.qty)
lp.PnL = lp.Value - lp.Cost
if lp.Cost != 0 {
@@ -203,10 +214,25 @@ func lkOnlinePortfolio(ctx context.Context, rc *RuntimeConfig, contract string)
if !asOf.IsZero() {
out.AsOf = asOf.Format("02.01.2006")
}
if len(fxUsed) > 0 {
var parts []string
for cur, px := range fxUsed {
p := cur + " " + fmtRate(px.FX) + " ₽"
if !px.FXAt.IsZero() {
p += " от " + px.FXAt.Format("02.01.2006")
}
parts = append(parts, p)
}
sort.Strings(parts)
out.Note = "выпуски с валютным номиналом пересчитаны по курсу " + strings.Join(parts, ", ")
}
lkPortfolioTotals(out)
return out, nil
}
// fmtRate — курс валюты с двумя знаками.
func fmtRate(v float64) string { return strconv.FormatFloat(v, 'f', 2, 64) }
// lkPortfolioTotals — итоги и доли.
func lkPortfolioTotals(p *LKPortfolio) {
p.Securities, p.Cost, p.PnL = 0, 0, 0