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

- 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
+91
View File
@@ -0,0 +1,91 @@
package lkgateway
// lkfx.go — курсы валют к рублю для оценки валютных выпусков.
//
// Номинал «замещающих» и еврооблигаций в экспорте QUIK задан в валюте (FaceUnit = USD, EUR,
// CNY), а цена — в процентах от него. Чтобы показать стоимость в рублях, нужен курс. В базе
// экспорта курсов нет (валютный рынок не выгружается), поэтому берём официальный курс из
// учётной системы: OD_VALUES_RATES с MARKET = 0 — сначала DEPO, затем SPECTRE, и берём более
// свежий. Дата курса показывается клиенту: курс может отставать, и выдавать оценку за
// сегодняшнюю нельзя.
import (
"context"
"database/sql"
"strings"
"sync"
"time"
)
// LKFxRate — курс валюты к рублю и дата, на которую он установлен.
type LKFxRate struct {
Rate float64
At time.Time
}
var fxCache = struct {
mu sync.Mutex
at time.Time
m map[string]LKFxRate
}{}
// lkFxRates — курсы валют (ключ — код валюты в верхнем регистре), кэш на час.
func lkFxRates(ctx context.Context, s FancyMSSQLSettings) map[string]LKFxRate {
fxCache.mu.Lock()
if fxCache.m != nil && time.Since(fxCache.at) < time.Hour {
m := fxCache.m
fxCache.mu.Unlock()
return m
}
fxCache.mu.Unlock()
out := map[string]LKFxRate{}
if strings.TrimSpace(s.Server) == "" {
return out
}
for _, db := range []string{s.DepoDB(), s.SpectreDB()} {
conn, err := fancyOpen(s, db)
if err != nil {
continue
}
cctx, cancel := context.WithTimeout(ctx, 15*time.Second)
rows, err := conn.QueryContext(cctx, `SELECT UPPER(LTRIM(RTRIM(v.SYSNAME))), r.OFICDATE, CAST(r.RATE AS float)
FROM OD_VALUES_RATES r JOIN OD_VALUES v ON v.ID = r.VALUE_ID
WHERE v.V_TYPE = 1 AND r.MARKET = 0 AND r.RATE > 0
AND r.OFICDATE = (SELECT MAX(x.OFICDATE) FROM OD_VALUES_RATES x WHERE x.VALUE_ID = r.VALUE_ID AND x.MARKET = 0 AND x.RATE > 0)`)
if err == nil {
for rows.Next() {
var code string
var at sql.NullTime
var rate float64
if rows.Scan(&code, &at, &rate) != nil || rate <= 0 {
continue
}
cur := LKFxRate{Rate: rate}
if at.Valid {
cur.At = at.Time
}
// из двух баз берём курс посвежее
if old, ok := out[code]; !ok || cur.At.After(old.At) {
out[code] = cur
}
}
rows.Close()
}
cancel()
conn.Close()
}
fxCache.mu.Lock()
fxCache.m, fxCache.at = out, time.Now()
fxCache.mu.Unlock()
return out
}
// lkFxFor — курс валюты к рублю: рубль — единица, неизвестная валюта — 0 (оценивать нельзя).
func lkFxFor(rates map[string]LKFxRate, cur string) LKFxRate {
c := strings.ToUpper(strings.TrimSpace(cur))
switch c {
case "", "SUR", "RUB", "RUR":
return LKFxRate{Rate: 1}
}
return rates[c]
}