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

- 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
+21 -3
View File
@@ -26,6 +26,11 @@ type SecurityPrice struct {
Accrued float64 `json:"accrued,omitempty"` // НКД на бумагу
InPct bool `json:"in_pct,omitempty"` // цена в процентах от номинала
ChangePc float64 `json:"change_pc,omitempty"` // изменение к закрытию, %
// FaceCur — валюта номинала (USD, EUR, CNY…); пусто или SUR — рубли. У «замещающих»
// и еврооблигаций номинал валютный, поэтому тело переводится в рубли по курсу FX.
FaceCur string `json:"face_cur,omitempty"`
FX float64 `json:"fx,omitempty"` // курс валюты номинала к рублю (1 для рублей)
FXAt time.Time `json:"fx_at,omitempty"` // дата курса
}
// Value — стоимость количества бумаг по этой цене.
@@ -34,7 +39,16 @@ func (p SecurityPrice) Value(qty float64) float64 {
return 0
}
if p.InPct && p.Face > 0 {
return qty*p.Face*p.Price/100 + qty*p.Accrued
fx := p.FX
if fx <= 0 {
if p.FaceCur == "" || strings.EqualFold(p.FaceCur, "SUR") || strings.EqualFold(p.FaceCur, "RUB") {
fx = 1
} else {
return 0 // валютный номинал без курса: оценку не выдумываем
}
}
// НКД экспорт отдаёт в рублях, тело — в процентах от номинала в валюте выпуска.
return qty*p.Face*p.Price/100*fx + qty*p.Accrued
}
return qty * p.Price
}
@@ -71,13 +85,14 @@ func lkPrices(ctx context.Context, rc *RuntimeConfig, isins []string) map[string
}
cctx, cancel := context.WithTimeout(ctx, 8*time.Second)
defer cancel()
fxRates := lkFxRates(ctx, rc.Snapshot().Fancy) // для выпусков с валютным номиналом
// Одна цена на бумагу: берём класс основных торгов (акции TQBR, ОФЗ TQOB,
// корпоративные облигации TQCB, фонды TQTF/TQIF), иначе — класс с торгами.
rows, err := p.Query(cctx, `
SELECT DISTINCT ON (s."ISINCode")
upper(s."ISINCode"), s."ClassCode", COALESCE(s."SecShortName", s."SecName", ''),
COALESCE(p.last, 0)::float8, COALESCE(p.prevprice, 0)::float8,
COALESCE(s."FaceValue", 0)::float8, COALESCE(p.accruedint, 0)::float8
COALESCE(s."FaceValue", 0)::float8, COALESCE(p.accruedint, 0)::float8, COALESCE(s."FaceUnit", '')
FROM "Securities" s JOIN "Params" p ON p."SecCode" = s."SecCode" AND p."ClassCode" = s."ClassCode"
WHERE s."ISINCode" <> '' AND (p.last > 0 OR p.prevprice > 0)
ORDER BY s."ISINCode",
@@ -90,7 +105,7 @@ func lkPrices(ctx context.Context, rc *RuntimeConfig, isins []string) map[string
all := map[string]SecurityPrice{}
for rows.Next() {
var sp SecurityPrice
if rows.Scan(&sp.ISIN, &sp.Board, &sp.Name, &sp.Price, &sp.Prev, &sp.Face, &sp.Accrued) != nil {
if rows.Scan(&sp.ISIN, &sp.Board, &sp.Name, &sp.Price, &sp.Prev, &sp.Face, &sp.Accrued, &sp.FaceCur) != nil {
continue
}
if sp.Price <= 0 {
@@ -100,6 +115,9 @@ func lkPrices(ctx context.Context, rc *RuntimeConfig, isins []string) map[string
if sp.Prev > 0 && sp.Price > 0 {
sp.ChangePc = (sp.Price - sp.Prev) / sp.Prev * 100
}
if fx := lkFxFor(fxRates, sp.FaceCur); fx.Rate > 0 {
sp.FX, sp.FXAt = fx.Rate, fx.At
}
all[sp.ISIN] = sp
}
rows.Close()