package lkgateway // lkfx.go — курсы валют к рублю для оценки валютных выпусков. // // Номинал «замещающих» и еврооблигаций в экспорте QUIK задан в валюте (FaceUnit = USD, EUR, // CNY), а цена — в процентах от него. Чтобы показать стоимость в рублях, нужен курс. В базе // экспорта курсов нет (валютный рынок не выгружается), поэтому берём официальный курс из // учётной системы: OD_VALUES_RATES с MARKET = 0 — сначала DEPO, затем SPECTRE, и берём более // свежий. Дата курса показывается клиенту: курс может отставать, и выдавать оценку за // сегодняшнюю нельзя. import ( "context" "database/sql" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql" "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 — курсы валют для оценки (ключ — код валюты в верхнем регистре). // Источник по умолчанию — Банк России (см. cbrfx.go); если интернета нет или // в настройках выбрана учётная система, берём OD_VALUES_RATES, а недостающие // валюты дополняем последним известным курсом из lk.fx_rates. func lkFxRates(ctx context.Context, rc *RuntimeConfig) map[string]LKFxRate { set := rc.Snapshot() out := map[string]LKFxRate{} if !strings.EqualFold(strings.TrimSpace(set.LK.FXSource), "accounting") { if cbr, cerr := cbrToday(ctx, rc); len(cbr) > 0 { return cbr } else if cerr != "" { // молча уходим на запасные источники: кабинет не должен падать // из-за недоступного сайта ЦБ _ = cerr } } for code, r := range lkFxAccounting(ctx, set.Fancy) { out[code] = r } for code, r := range lkFxLoad(ctx, rc, time.Time{}) { if old, ok := out[code]; !ok || r.At.After(old.At) { out[code] = r } } return out } // lkFxAccounting — курсы из учётной системы (OD_VALUES_RATES, MARKET = 0), кэш на час. func lkFxAccounting(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, fsql.Fx01) 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] } // lkFxReset — сбросить кэши курсов (после смены источника или ручного обновления). func lkFxReset() { fxCache.mu.Lock() fxCache.m, fxCache.at = nil, time.Time{} fxCache.mu.Unlock() cbrCache.mu.Lock() cbrCache.at, cbrCache.err = time.Time{}, "" cbrCache.mu.Unlock() }