Files
Bridge-and-Join-s/internal/lkgateway/lkfx.go
T
zuevav 9712b4feda Кабинет: верная оценка валютных выпусков и читаемые числа
- 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
2026-09-18 18:15:38 +03:00

92 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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]
}