Files
Bridge-and-Join-s/internal/lkgateway/lkclient.go
T
zuevav 02fabb483d Кабинет клиента: портфель как у брокера — «Онлайн» по 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
2026-09-18 15:44:53 +03:00

103 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}