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
177 lines
6.5 KiB
Go
177 lines
6.5 KiB
Go
package lkgateway
|
|
|
|
// security_card.go — карточка ценной бумаги для кабинета клиента: реквизиты выпуска из
|
|
// справочника депозитария (OD_VALUES + действующая версия OD_SHARES), график купонов и
|
|
// оферт (OD_COUPONS, OD_OFFERS), цена из базы экспорта QUIK и позиция клиента.
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// SecurityPayment — событие по бумаге: купон, оферта, погашение.
|
|
type SecurityPayment struct {
|
|
Date string `json:"date"` // ДД.ММ.ГГГГ
|
|
Kind string `json:"kind"` // купон | оферта | погашение
|
|
Amount float64 `json:"amount,omitempty"` // на одну бумагу
|
|
Percent float64 `json:"percent,omitempty"` // ставка купона, % годовых
|
|
FixDate string `json:"fix_date,omitempty"`
|
|
Paid bool `json:"paid,omitempty"`
|
|
}
|
|
|
|
// SecurityCard — карточка бумаги.
|
|
type SecurityCard struct {
|
|
ISIN string `json:"isin"`
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind,omitempty"` // акция | облигация | пай | бумага
|
|
Issuer string `json:"issuer,omitempty"` // эмитент
|
|
RegNumber string `json:"reg_number,omitempty"`
|
|
CFI string `json:"cfi,omitempty"`
|
|
Face float64 `json:"face,omitempty"` // номинал
|
|
Maturity string `json:"maturity,omitempty"` // дата погашения
|
|
Accepted bool `json:"accepted"` // принята на обслуживание
|
|
QualOnly bool `json:"qual_only,omitempty"` // только для квалифицированных инвесторов
|
|
QualNote string `json:"qual_note,omitempty"`
|
|
|
|
Price float64 `json:"price,omitempty"`
|
|
PriceIn string `json:"price_in,omitempty"` // «₽ за штуку» | «% от номинала»
|
|
Board string `json:"board,omitempty"` // класс торгов
|
|
ChangePct float64 `json:"change_pct,omitempty"` // изменение к закрытию
|
|
|
|
Position *ClientPosition `json:"position,omitempty"`
|
|
Payments []SecurityPayment `json:"payments,omitempty"`
|
|
Note string `json:"note,omitempty"` // почему чего-то нет
|
|
}
|
|
|
|
// securityCard — собирает карточку. Позиция клиента передаётся снаружи (её уже знает
|
|
// обработчик портфеля), справочные данные читаются из учётной системы.
|
|
func securityCard(ctx context.Context, rc *RuntimeConfig, isin string, pos *ClientPosition) (SecurityCard, error) {
|
|
isin = strings.ToUpper(strings.TrimSpace(isin))
|
|
card := SecurityCard{ISIN: isin, Position: pos}
|
|
if pos != nil {
|
|
card.Name = pos.SecurityName
|
|
}
|
|
if isin == "" {
|
|
return card, fmt.Errorf("не указана бумага")
|
|
}
|
|
if px := lkPrices(ctx, rc, []string{isin}); len(px) > 0 {
|
|
p := px[isin]
|
|
card.Price, card.Board, card.ChangePct = p.Price, p.Board, p.ChangePc
|
|
card.PriceIn = "₽ за штуку"
|
|
if p.InPct {
|
|
card.PriceIn = "% от номинала"
|
|
}
|
|
if card.Name == "" {
|
|
card.Name = p.Name
|
|
}
|
|
}
|
|
|
|
fs := rc.Snapshot().Fancy
|
|
if strings.TrimSpace(fs.Server) == "" {
|
|
card.Note = "справочник депозитария недоступен"
|
|
return card, nil
|
|
}
|
|
db, dctx, cancel, errS := depoOpen(fs)
|
|
if errS != "" {
|
|
card.Note = errS
|
|
return card, nil
|
|
}
|
|
defer cancel()
|
|
defer db.Close()
|
|
_ = dctx
|
|
|
|
var valID int64
|
|
var name, cfi string
|
|
var vType, isIn sql.NullInt64
|
|
err := db.QueryRowContext(dctx, fsql.Sec01,
|
|
sql.Named("p1", isin)).Scan(&valID, &name, &cfi, &vType, &isIn)
|
|
if err != nil {
|
|
card.Note = "бумаги нет в справочнике депозитария"
|
|
return card, nil
|
|
}
|
|
if name != "" {
|
|
card.Name = name
|
|
}
|
|
card.CFI, card.Accepted = cfi, isIn.Valid && isIn.Int64 == 1
|
|
card.Kind = securityKind(cfi)
|
|
|
|
var issuer sql.NullInt64
|
|
var nominal sql.NullFloat64
|
|
var regNum sql.NullString
|
|
var death sql.NullTime
|
|
err = db.QueryRowContext(dctx, fsql.Sec02, sql.Named("p1", valID)).Scan(&issuer, &nominal, ®Num, &death)
|
|
if err == nil {
|
|
card.Face = nominal.Float64
|
|
card.RegNumber = strings.TrimSpace(regNum.String)
|
|
if death.Valid && death.Time.Year() < 9000 {
|
|
card.Maturity = death.Time.Format("02.01.2006")
|
|
}
|
|
if issuer.Valid && issuer.Int64 > 0 {
|
|
var iname string
|
|
if db.QueryRowContext(dctx, fsql.Sec03,
|
|
sql.Named("p1", issuer.Int64)).Scan(&iname) == nil {
|
|
card.Issuer = strings.TrimSpace(iname)
|
|
}
|
|
}
|
|
}
|
|
card.QualOnly, card.QualNote = depoSecurityQualOnly(dctx, db, valID)
|
|
|
|
// График: купоны за полгода назад и вперёд, ближайшие оферты, погашение.
|
|
// Дата выплаты купона: P_DATE, а пока купон не выплачен — конец купонного периода.
|
|
rows, err := db.QueryContext(dctx, fsql.Sec04, sql.Named("p1", valID))
|
|
if err == nil {
|
|
for rows.Next() {
|
|
var pd, fx sql.NullTime
|
|
var summa, pct float64
|
|
var isPay sql.NullInt64
|
|
if rows.Scan(&pd, &summa, &pct, &fx, &isPay) != nil || !pd.Valid {
|
|
continue
|
|
}
|
|
p := SecurityPayment{Date: pd.Time.Format("02.01.2006"), Kind: "купон", Amount: summa, Percent: pct,
|
|
Paid: pd.Time.Before(time.Now())}
|
|
if fx.Valid && fx.Time.Year() < 9000 {
|
|
p.FixDate = fx.Time.Format("02.01.2006")
|
|
}
|
|
card.Payments = append(card.Payments, p)
|
|
}
|
|
rows.Close()
|
|
}
|
|
orows, err := db.QueryContext(dctx, fsql.Sec05, sql.Named("p1", valID))
|
|
if err == nil {
|
|
for orows.Next() {
|
|
var pd sql.NullTime
|
|
var price float64
|
|
if orows.Scan(&pd, &price) == nil && pd.Valid {
|
|
card.Payments = append(card.Payments, SecurityPayment{Date: pd.Time.Format("02.01.2006"), Kind: "оферта", Amount: price})
|
|
}
|
|
}
|
|
orows.Close()
|
|
}
|
|
if card.Maturity != "" {
|
|
card.Payments = append(card.Payments, SecurityPayment{Date: card.Maturity, Kind: "погашение", Amount: card.Face})
|
|
}
|
|
return card, nil
|
|
}
|
|
|
|
// securityKind — вид бумаги по коду CFI (первая буква классификации).
|
|
func securityKind(cfi string) string {
|
|
if cfi == "" {
|
|
return ""
|
|
}
|
|
switch strings.ToUpper(cfi[:1]) {
|
|
case "E":
|
|
return "акция"
|
|
case "D":
|
|
return "облигация"
|
|
case "C":
|
|
return "пай фонда"
|
|
case "R":
|
|
return "право"
|
|
}
|
|
return "ценная бумага"
|
|
}
|