7ce4c2c79f
Реализация варианта B из аудита защиты знаний. Прямой garble всего bj-server ломает html/template (он обращается к полям структур по имени, garble их переименовывает). Решение — селективная обфускация: GOGARBLE обфусцирует ТОЛЬКО пакет с секретами, шаблоны и обработчики не затрагиваются. 95 SQL-запросов к DEPO/SPECTRE/QUIK и трансферным таблицам FTV_M2M вынесены из lkgateway в отдельный пакет internal/lkgateway/fsql как переменные (именно var, не const: const компилятор вставил бы обратно в необфусцируемый пакет, и шифрование бы не сработало — проверено). Сами обработчики, типы и параметры sql.Named остались на месте; пакет держит только строки запросов. build-release.sh собирает bj-server с `GOGARBLE=.../fsql garble -literals` (нужен отдельный SDK go1.26 — модуль-кэшный тулчейн garble не подходит); если garble/go1.26 не найдены — обычная сборка с предупреждением. Проверено на реальном garble-бинаре (запускается, /admin и страницы на структурах работают): рецепт чтения чужих систем из бинаря исчез — OD_RESTS 13→0, OD_WIRING 5→0, OD_TURNS 1→0, OD_BALANCES 3→0, D_B_CONTRACTS 1→0, OD_CLIENTS_PORTS 10→0, DepoLimits/MoneyLimits →0. Остаток FTV_M2M — упоминания имён таблиц в операторских embed-шаблонах и логах, не сам рецепт. Стенд собран обычной сборкой (без garble) — трассировки остаются читаемыми. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
161 lines
6.3 KiB
Go
161 lines
6.3 KiB
Go
package lkgateway
|
|
|
|
// prices.go — рыночные цены бумаг из базы экспорта QUIK (таблицы Securities и Params,
|
|
// пакет данных «Цены для оценки портфеля»). Нужны кабинету, чтобы показать стоимость
|
|
// позиции и портфеля. Подключение берём из настроек «НПР / УДС» — это та же база
|
|
// экспорта; без неё кабинет просто не показывает оценку.
|
|
//
|
|
// Цена облигации приходит в процентах от номинала, акции и пая — в рублях за штуку.
|
|
// Оценка: облигация = количество × номинал × цена / 100 + НКД × количество.
|
|
|
|
import (
|
|
"context"
|
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// SecurityPrice — цена одной бумаги на момент среза.
|
|
type SecurityPrice struct {
|
|
ISIN string `json:"isin"`
|
|
Board string `json:"board"` // класс QUIK, откуда взята цена
|
|
Name string `json:"name,omitempty"` // краткое наименование в торговой системе
|
|
Price float64 `json:"price"` // последняя цена (для облигаций — процент номинала)
|
|
Prev float64 `json:"prev,omitempty"` // цена предыдущего закрытия
|
|
Face float64 `json:"face,omitempty"` // номинал
|
|
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 — стоимость количества бумаг по этой цене.
|
|
func (p SecurityPrice) Value(qty float64) float64 {
|
|
if p.Price <= 0 || qty == 0 {
|
|
return 0
|
|
}
|
|
if p.InPct && p.Face > 0 {
|
|
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
|
|
}
|
|
|
|
// pricesCache — цены живут минуту: страницу кабинета открывают часто, а цикл экспорта — 30 с.
|
|
var (
|
|
priceMu sync.Mutex
|
|
priceAt time.Time
|
|
priceLast map[string]SecurityPrice
|
|
)
|
|
|
|
// lkPrices — цены по списку ISIN. Пустая карта, если база экспорта не настроена или
|
|
// пакет «Цены» не включён: кабинет в этом случае показывает реестр без оценки.
|
|
func lkPrices(ctx context.Context, rc *RuntimeConfig, isins []string) map[string]SecurityPrice {
|
|
out := map[string]SecurityPrice{}
|
|
if rc == nil || len(isins) == 0 {
|
|
return out
|
|
}
|
|
priceMu.Lock()
|
|
if time.Since(priceAt) < time.Minute && priceLast != nil {
|
|
for _, isin := range isins {
|
|
if p, ok := priceLast[strings.ToUpper(isin)]; ok {
|
|
out[p.ISIN] = p
|
|
}
|
|
}
|
|
priceMu.Unlock()
|
|
return out
|
|
}
|
|
priceMu.Unlock()
|
|
|
|
p, err := nprDB(rc)
|
|
if err != nil {
|
|
return out
|
|
}
|
|
cctx, cancel := context.WithTimeout(ctx, 8*time.Second)
|
|
defer cancel()
|
|
fxRates := lkFxRates(ctx, rc) // для выпусков с валютным номиналом
|
|
// Одна цена на бумагу: берём класс основных торгов (акции TQBR, ОФЗ TQOB,
|
|
// корпоративные облигации TQCB, фонды TQTF/TQIF), иначе — класс с торгами.
|
|
rows, err := p.Query(cctx, fsql.Pr01)
|
|
if err != nil {
|
|
return out
|
|
}
|
|
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, &sp.FaceCur) != nil {
|
|
continue
|
|
}
|
|
if sp.Price <= 0 {
|
|
sp.Price = sp.Prev
|
|
}
|
|
sp.InPct = sp.Face > 0 && (sp.Board == "TQOB" || sp.Board == "TQCB" || strings.HasSuffix(sp.Board, "_BND"))
|
|
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()
|
|
if len(all) == 0 {
|
|
return out
|
|
}
|
|
priceMu.Lock()
|
|
priceLast, priceAt = all, time.Now()
|
|
priceMu.Unlock()
|
|
for _, isin := range isins {
|
|
if sp, ok := all[strings.ToUpper(isin)]; ok {
|
|
out[sp.ISIN] = sp
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// lkPriceReset — сбросить кэш цен (после смены настроек подключения).
|
|
func lkPriceReset() {
|
|
priceMu.Lock()
|
|
priceLast, priceAt = nil, time.Time{}
|
|
priceMu.Unlock()
|
|
}
|
|
|
|
// lkApplyPrices — проставляет позициям цену и стоимость, возвращает оценку портфеля
|
|
// и число бумаг, по которым цена нашлась.
|
|
func lkApplyPrices(ctx context.Context, rc *RuntimeConfig, pos []ClientPosition) (total float64, priced int) {
|
|
if len(pos) == 0 {
|
|
return 0, 0
|
|
}
|
|
isins := make([]string, 0, len(pos))
|
|
for _, p := range pos {
|
|
if p.ISIN != "" {
|
|
isins = append(isins, p.ISIN)
|
|
}
|
|
}
|
|
px := lkPrices(ctx, rc, isins)
|
|
for i := range pos {
|
|
sp, ok := px[strings.ToUpper(pos[i].ISIN)]
|
|
if !ok || sp.Price <= 0 {
|
|
continue
|
|
}
|
|
pos[i].Price, pos[i].PriceAt, pos[i].ChangePct = sp.Price, sp.Board, sp.ChangePc
|
|
pos[i].Value = sp.Value(pos[i].Quantity)
|
|
total += pos[i].Value
|
|
priced++
|
|
}
|
|
return total, priced
|
|
}
|