510a34b7bc
Курсы валют. Оценивать замещающие и еврооблигации по курсу из копии учётной системы было нельзя: в DEPO он от 31.05.2026, в SPECTRE — от 18.07.2026. Берём официальный курс Банка России (открытый XML, без ключа — тем же каналом пользуется Инвест-хаб) и складываем по дням в lk.fx_rates: без интернета кабинет считает по последнему известному курсу, а не отказывается оценивать. Источник переключается в настройках («Данные кабинета»), там же кнопка «Обновить курсы сейчас». Портфель БР-719: 560,3 млн ₽ против 522 млн по старому курсу. История стоимости. Ни QUIK, ни SPECTRE её не хранят, поэтому раз в день после закрытия торгов считаем стоимость каждого портфеля по экспорту QUIK и пишем строку в lk.portfolio_daily (257 портфелей за 14 секунд). Кабинет рисует по ним график на странице портфеля — SVG собирается на сервере, скриптов в кабинете по-прежнему нет. Режим по умолчанию — «автоматически»: ведём, если настроен экспорт QUIK, потому что задним числом историю не восстановить. Операции. Связка «JOIN OD_RESTS ON sr.ID = CASE WHEN w.D_REST IN (…)» заставляла SQL Server разбирать список счетов на каждой строке журнала проводок: год данных читался 6–7 секунд. Обе стороны проводки выбираются заранее двумя ветвями UNION ALL, индексы IX_WIRING_D_REST_WIRDATE и IX_WIRING_K_REST_WIRDATE отрабатывают поиском — 89 мс на тех же 64 строках. Период по умолчанию вернули к году, ответ кэшируется на пять минут. Профиль: почта, телефон и прошлый вход из анкеты — клиент видит, куда с ним свяжется депозитарий, и заметит устаревшие данные. Правка только через менеджера. Заказ документов из кабинета убран совсем (форма была убрана раньше, теперь нет и обработчика). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
118 lines
4.8 KiB
Go
118 lines
4.8 KiB
Go
package lkgateway
|
|
|
|
// lkdata.go — карточка «Данные кабинета» в настройках: откуда берём курсы валют
|
|
// и ведём ли ежедневный срез стоимости портфелей (для графика у клиента).
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// LKDataView — состояние источников данных кабинета для страницы настроек.
|
|
type LKDataView struct {
|
|
FXSource string // cbr | accounting
|
|
FXAt string // дата последнего известного курса
|
|
FXCount int // сколько валют в последнем комплекте
|
|
FXNote string // пояснение/ошибка связи с Банком России
|
|
SnapshotMode string // "" (авто) | on | off
|
|
SnapshotOn bool // ведём ли по факту
|
|
SnapshotAt string // ЧЧ:ММ
|
|
Snapshots int // сколько дней истории накоплено
|
|
SnapLast string // последний снятый срез
|
|
SnapRows int // строк в последнем срезе
|
|
}
|
|
|
|
// lkDataView — собрать состояние без обращения в интернет (карточку открывают часто).
|
|
func lkDataView(rc *RuntimeConfig) LKDataView {
|
|
s := rc.Snapshot().LK
|
|
hour, min := lkSnapshotTime(s)
|
|
v := LKDataView{FXSource: strings.TrimSpace(s.FXSource), SnapshotMode: strings.TrimSpace(s.SnapshotMode),
|
|
SnapshotOn: lkSnapshotEnabled(rc), SnapshotAt: fmt.Sprintf("%02d:%02d", hour, min)}
|
|
if v.FXSource == "" {
|
|
v.FXSource = "cbr"
|
|
}
|
|
db, err := lkDB(rc)
|
|
if err != nil {
|
|
v.FXNote = "база bj недоступна: " + err.Error()
|
|
return v
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
var day *time.Time
|
|
var n int
|
|
if db.QueryRowContext(ctx, `SELECT max(day) FROM lk.fx_rates`).Scan(&day) == nil && day != nil {
|
|
v.FXAt = day.Format("02.01.2006")
|
|
_ = db.QueryRowContext(ctx, `SELECT count(*) FROM lk.fx_rates WHERE day = $1`, *day).Scan(&n)
|
|
v.FXCount = n
|
|
}
|
|
var sday *time.Time
|
|
if db.QueryRowContext(ctx, `SELECT count(DISTINCT day), max(day) FROM lk.portfolio_daily`).Scan(&v.Snapshots, &sday) == nil && sday != nil {
|
|
v.SnapLast = sday.Format("02.01.2006")
|
|
_ = db.QueryRowContext(ctx, `SELECT count(*) FROM lk.portfolio_daily WHERE day = $1`, *sday).Scan(&v.SnapRows)
|
|
}
|
|
return v
|
|
}
|
|
|
|
// lkDataSave — POST /admin/setup/lk/data: источник курсов и ежедневный срез.
|
|
func (h *setupHandlers) lkDataSave(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
switch r.FormValue("op") {
|
|
case "fx_now": // проверить связь с Банком России и обновить курсы
|
|
at, rates, err := cbrFetch(r.Context(), time.Time{})
|
|
if err != nil {
|
|
setupFlash(w, r, "Банк России недоступен: "+err.Error()+". Курсы останутся прежними.")
|
|
return
|
|
}
|
|
lkFxSave(r.Context(), h.rc, at, "cbr", rates)
|
|
lkFxReset()
|
|
setupFlash(w, r, fmt.Sprintf("Курсы Банка России на %s получены: валют %d.", at.Format("02.01.2006"), len(rates)))
|
|
return
|
|
case "snapshot_now": // снять срез стоимости прямо сейчас
|
|
go func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
|
defer cancel()
|
|
n, err := lkSnapshotRun(ctx, h.rc, time.Now())
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = n
|
|
}()
|
|
setupFlash(w, r, "Срез стоимости портфелей снимается — на 1300 договоров это несколько минут; обновите страницу.")
|
|
return
|
|
}
|
|
s := h.rc.Snapshot().LK
|
|
if v := strings.TrimSpace(r.FormValue("fx_source")); v == "cbr" || v == "accounting" {
|
|
s.FXSource = v
|
|
}
|
|
switch v := strings.TrimSpace(r.FormValue("snapshot_mode")); v {
|
|
case "", "on", "off":
|
|
s.SnapshotMode = v
|
|
}
|
|
if v := strings.TrimSpace(r.FormValue("snapshot_at")); len(v) == 5 && v[2] == ':' {
|
|
s.SnapshotAt = v
|
|
}
|
|
if err := h.rc.UpdateLK(s); err != nil {
|
|
setupFlash(w, r, "Данные кабинета: ошибка сохранения: "+err.Error())
|
|
return
|
|
}
|
|
lkFxReset()
|
|
src := "Банк России"
|
|
if s.FXSource == "accounting" {
|
|
src = "учётная система"
|
|
}
|
|
msg := "Курсы валют: " + src + "."
|
|
if s.SnapshotMode == "off" {
|
|
msg += " Ежедневный срез выключен — график в кабинете обновляться не будет."
|
|
} else {
|
|
hour, min := lkSnapshotTime(s)
|
|
msg += fmt.Sprintf(" Срез стоимости портфелей — ежедневно в %02d:%02d.", hour, min)
|
|
}
|
|
setupFlash(w, r, msg)
|
|
}
|