02fabb483d
- 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
103 lines
3.7 KiB
Go
103 lines
3.7 KiB
Go
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
|
||
}
|