Кабинет клиента: портфель как у брокера — «Онлайн» по QUIK, «Архив» на дату и операции по SPECTRE

- lkclient.go: договор → код QUIK (БР-1545 → BR1545, +OUT), договор → D_B_CONTRACTS SPECTRE
- lkonline.go: позиции DepoLimits (T2, свежая строка на счёт), средняя цена покупки, деньги
  MoneyLimits по валютам, цены и P&L; стоимость, доли
- lkarchive.go: портфель на дату по остаткам OD_RESTS (REG_3 = договор, раздел 39, счета
  50/51/52/55 и 58.01/58.02; 58.05 НКД не показываем) с откатом по OD_TURNS после даты
  (индекс REST+WIRDATE, знак TYPE_ проверен: сумма оборотов = остаток); оценка по котировке
  OD_VALUES_RATES ≤ 90 дней (в рублях за бумагу), иначе балансовая; операции из OD_WIRING
  (покупка/продажа/ввод/вывод/погашение/конвертация, продажа — по счёту ВЫРУЧКА) и
  пополнения/выводы денег; рецепт — InvestControl Hub (build_portfolio_base, deals_report)
- API: /api/lk/portfolio2?view=online|archive&date=, /api/lk/ops (сделки + выплаты дохода);
  без клиента в QUIK онлайн падает на брокерский учёт за сегодня
- кабинет: разделы Портфель (вкладки Онлайн/Архив с датой), Операции, Поручения, Документы,
  Профиль; депозитарные реестр и доходы из клиентского вида убраны (/portfolio → /,
  /income → /operations); проверено на БР-704 и БР-1545
- настройка Fancy.SpectreDatabase (по умолчанию BJ_SPECTRE_DATA)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
zuevav
2026-09-18 15:44:53 +03:00
parent 05d02f22e6
commit 02fabb483d
9 changed files with 1268 additions and 274 deletions
+102
View File
@@ -0,0 +1,102 @@
package lkgateway
// lkclient.go — как один и тот же клиент называется в разных системах.
//
// В учётных записях кабинета клиент задан номером договора, как в DEPO: «БР-1545», «БРИИС-514».
// В экспорте QUIK тот же клиент — код латиницей без дефиса: BR1545, BRIIS514; внебиржевой
// раздел — с суффиксом OUT. В SPECTRE договор — строка D_B_CONTRACTS.NAME вида «БР-1545 ФИО».
import (
"context"
"database/sql"
"strings"
"sync"
"time"
)
// lkContractNumber — номер договора без ФИО: «БР-1545 Иванов Иван» → «БР-1545».
func lkContractNumber(contract string) string {
c := strings.TrimSpace(contract)
if i := strings.IndexByte(c, ' '); i > 0 {
c = c[:i]
}
return strings.ToUpper(c)
}
// lkQuikCodes — коды клиента в QUIK по номеру договора: основной и внебиржевой раздел.
func lkQuikCodes(contract string) []string {
num := lkContractNumber(contract)
if num == "" {
return nil
}
repl := strings.NewReplacer("-", "", "А", "A", "Б", "B", "В", "V", "Д", "D", "И", "I", "С", "S", "Р", "R", "У", "U", "Т", "T", "К", "K", "М", "M", "Н", "N", "О", "O", "Е", "E", "П", "P", "Ф", "F", "Ц", "C")
code := repl.Replace(num)
return []string{code, code + "OUT"}
}
// spectreContract — договор клиента в SPECTRE.
type spectreContract struct {
DOC int64
Investor int64
Name string
Open time.Time
Closed bool
}
var spectreContractCache = struct {
mu sync.Mutex
m map[string]spectreContract
at map[string]time.Time
}{m: map[string]spectreContract{}, at: map[string]time.Time{}}
// spectreOpen — подключение к копии SPECTRE тем же логином, что и к DEPO.
func spectreOpen(s FancyMSSQLSettings) (*sql.DB, error) {
db := strings.TrimSpace(s.SpectreDB())
return fancyOpen(s, db)
}
// SpectreDB — база SPECTRE рядом с DEPO: по умолчанию BJ_SPECTRE_DATA.
func (s FancyMSSQLSettings) SpectreDB() string {
if v := strings.TrimSpace(s.SpectreDatabase); v != "" {
return v
}
return "BJ_SPECTRE_DATA"
}
// spectreContractFor — договор SPECTRE по номеру договора клиента (кэш 10 минут).
func spectreContractFor(ctx context.Context, s FancyMSSQLSettings, contract string) (spectreContract, bool) {
num := lkContractNumber(contract)
if num == "" || strings.TrimSpace(s.Server) == "" {
return spectreContract{}, false
}
spectreContractCache.mu.Lock()
if c, ok := spectreContractCache.m[num]; ok && time.Since(spectreContractCache.at[num]) < 10*time.Minute {
spectreContractCache.mu.Unlock()
return c, c.DOC != 0
}
spectreContractCache.mu.Unlock()
conn, err := spectreOpen(s)
if err != nil {
return spectreContract{}, false
}
defer conn.Close()
cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
var c spectreContract
var open, closed sql.NullTime
var state sql.NullInt64
err = conn.QueryRowContext(cctx, `SELECT TOP 1 DOC, INVESTOR, NAME, C_OPEN, C_CLOSE, C_STATE FROM D_B_CONTRACTS
WHERE UPPER(NAME) = @n OR UPPER(NAME) LIKE @n + ' %' ORDER BY C_STATE DESC, DOC DESC`, sql.Named("n", num)).
Scan(&c.DOC, &c.Investor, &c.Name, &open, &closed, &state)
if err == nil {
c.Name = strings.TrimSpace(c.Name)
if open.Valid {
c.Open = open.Time
}
c.Closed = closed.Valid && closed.Time.Year() < 9000 && closed.Time.Before(time.Now())
}
spectreContractCache.mu.Lock()
spectreContractCache.m[num], spectreContractCache.at[num] = c, time.Now()
spectreContractCache.mu.Unlock()
return c, err == nil && c.DOC != 0
}