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
217 lines
7.4 KiB
Go
217 lines
7.4 KiB
Go
package lkgateway
|
|
|
|
// lksnapshot.go — ежедневный срез стоимости портфелей для графика в кабинете.
|
|
//
|
|
// Ни QUIK, ни SPECTRE не хранят историю стоимости: экспорт QUIK показывает позиции
|
|
// «сейчас», SPECTRE — остатки на дату, но переоценивать каждый день задним числом
|
|
// дорого и всё равно неточно (котировки прошлых дней в копии не полны). Поэтому раз
|
|
// в день, после закрытия торгов, считаем стоимость каждого портфеля по экспорту QUIK
|
|
// и складываем одну строку на договор в lk.portfolio_daily. Из этих строк кабинет
|
|
// рисует график: чем дольше работает система, тем длиннее история.
|
|
|
|
import (
|
|
"context"
|
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var lkSnapshotSchema = fsql.Sn01
|
|
|
|
// lkSnapshotEnabled — ведём ли историю стоимости. По умолчанию (режим не выбран)
|
|
// ведём, если настроен экспорт QUIK: история накапливается только вперёд, и
|
|
// упущенные дни потом не восстановить.
|
|
func lkSnapshotEnabled(rc *RuntimeConfig) bool {
|
|
switch strings.ToLower(strings.TrimSpace(rc.Snapshot().LK.SnapshotMode)) {
|
|
case "on":
|
|
return true
|
|
case "off":
|
|
return false
|
|
}
|
|
_, err := nprDB(rc)
|
|
return err == nil
|
|
}
|
|
|
|
// lkSnapshotTime — когда снимаем срез (по умолчанию 20:30, после закрытия основной сессии).
|
|
func lkSnapshotTime(s LKSettings) (hour, min int) {
|
|
hour, min = 20, 30
|
|
v := strings.TrimSpace(s.SnapshotAt)
|
|
if len(v) == 5 && v[2] == ':' {
|
|
h := int(v[0]-'0')*10 + int(v[1]-'0')
|
|
m := int(v[3]-'0')*10 + int(v[4]-'0')
|
|
if h >= 0 && h < 24 && m >= 0 && m < 60 {
|
|
hour, min = h, m
|
|
}
|
|
}
|
|
return hour, min
|
|
}
|
|
|
|
// LKDailyPoint — точка графика стоимости портфеля.
|
|
type LKDailyPoint struct {
|
|
Day string `json:"day"` // 2026-09-18
|
|
Value float64 `json:"value"` // бумаги, ₽
|
|
Cash float64 `json:"cash"` // деньги в рублях
|
|
Total float64 `json:"total"` // бумаги + деньги
|
|
Positions int `json:"positions"`
|
|
}
|
|
|
|
// lkSnapshotHistory — история стоимости портфеля за последние days дней.
|
|
func lkSnapshotHistory(ctx context.Context, rc *RuntimeConfig, contract string, days int) ([]LKDailyPoint, error) {
|
|
db, err := lkDB(rc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
num := lkContractNumber(contract)
|
|
if num == "" {
|
|
return nil, nil
|
|
}
|
|
if days <= 0 || days > 3650 {
|
|
days = 365
|
|
}
|
|
cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
rows, err := db.QueryContext(cctx, fsql.Sn02, num, days)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []LKDailyPoint
|
|
for rows.Next() {
|
|
var p LKDailyPoint
|
|
var day time.Time
|
|
if rows.Scan(&day, &p.Value, &p.Cash, &p.Positions) != nil {
|
|
continue
|
|
}
|
|
p.Day = day.Format("2006-01-02")
|
|
p.Total = p.Value + p.Cash
|
|
out = append(out, p)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// lkSnapshotSave — записать срез одного портфеля за день (повторный запуск перезаписывает).
|
|
func lkSnapshotSave(ctx context.Context, rc *RuntimeConfig, day time.Time, contract string, p *LKPortfolio) error {
|
|
db, err := lkDB(rc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
_, err = db.ExecContext(cctx, fsql.Sn03,
|
|
day.Format("2006-01-02"), lkContractNumber(contract), p.Securities, p.CashRUB,
|
|
len(p.Positions), p.Priced, p.Source)
|
|
return err
|
|
}
|
|
|
|
// lkSnapshotContracts — чьи портфели снимаем: договоры, по которым в экспорте QUIK
|
|
// есть ненулевые позиции. Берём коды клиентов из экспорта и переводим их в номера
|
|
// договоров депозитария (BR746 → БР-746), иначе пришлось бы перебирать 1300 договоров.
|
|
func lkSnapshotContracts(ctx context.Context, rc *RuntimeConfig) []string {
|
|
p, err := nprDB(rc)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
rows, err := p.Query(cctx, fsql.Sn04)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer rows.Close()
|
|
live := map[string]bool{}
|
|
for rows.Next() {
|
|
var code string
|
|
if rows.Scan(&code) == nil {
|
|
live[strings.ToUpper(strings.TrimSpace(code))] = true
|
|
}
|
|
}
|
|
// номер договора знает только депозитарий: сопоставляем его коды с живыми в QUIK
|
|
var out []string
|
|
seen := map[string]bool{}
|
|
for _, full := range depoSuggestContracts(ctx, rc.Snapshot().Fancy) {
|
|
num := lkContractNumber(full)
|
|
if num == "" || seen[num] {
|
|
continue
|
|
}
|
|
for _, code := range lkQuikCodes(num) {
|
|
if live[strings.ToUpper(code)] {
|
|
seen[num] = true
|
|
out = append(out, num)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// lkSnapshotRun — один проход: считает и сохраняет срез за день по всем портфелям.
|
|
func lkSnapshotRun(ctx context.Context, rc *RuntimeConfig, day time.Time) (int, error) {
|
|
contracts := lkSnapshotContracts(ctx, rc)
|
|
saved := 0
|
|
for _, num := range contracts {
|
|
select {
|
|
case <-ctx.Done():
|
|
return saved, ctx.Err()
|
|
default:
|
|
}
|
|
pf, err := lkOnlinePortfolio(ctx, rc, num)
|
|
if err != nil || pf == nil {
|
|
continue
|
|
}
|
|
if err := lkSnapshotSave(ctx, rc, day, num, pf); err == nil {
|
|
saved++
|
|
}
|
|
}
|
|
return saved, nil
|
|
}
|
|
|
|
// lkSnapshotLoop — раз в день в назначенное время снимает срез. Если сервер был
|
|
// выключен и срез за сегодня не снят, а время уже прошло — снимаем при старте.
|
|
func (s *Server) lkSnapshotLoop(ctx context.Context) {
|
|
t := time.NewTicker(5 * time.Minute)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-t.C:
|
|
}
|
|
if !lkSnapshotEnabled(s.rc) {
|
|
continue
|
|
}
|
|
set := s.rc.Snapshot()
|
|
hour, min := lkSnapshotTime(set.LK)
|
|
now := time.Now()
|
|
if now.Hour() < hour || (now.Hour() == hour && now.Minute() < min) {
|
|
continue
|
|
}
|
|
day := now.Truncate(24 * time.Hour)
|
|
if lkSnapshotDone(ctx, s.rc, now) {
|
|
continue
|
|
}
|
|
n, err := lkSnapshotRun(ctx, s.rc, day)
|
|
if err != nil {
|
|
log.Printf("кабинет: срез стоимости портфелей прерван: %v", err)
|
|
continue
|
|
}
|
|
log.Printf("кабинет: срез стоимости портфелей за %s — %d договоров", now.Format("02.01.2006"), n)
|
|
}
|
|
}
|
|
|
|
// lkSnapshotDone — срез за этот день уже снят.
|
|
func lkSnapshotDone(ctx context.Context, rc *RuntimeConfig, day time.Time) bool {
|
|
db, err := lkDB(rc)
|
|
if err != nil {
|
|
return true // без базы срез всё равно не сохранить, и жаловаться каждые 5 минут незачем
|
|
}
|
|
cctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
var n int
|
|
if db.QueryRowContext(cctx, fsql.Sn05,
|
|
day.Format("2006-01-02")).Scan(&n) != nil {
|
|
return true
|
|
}
|
|
return n > 0
|
|
}
|