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
573 lines
20 KiB
Go
573 lines
20 KiB
Go
package lkgateway
|
|
|
|
// lkarchive.go — портфель клиента на дату и операции по брокерскому учёту (SPECTRE).
|
|
//
|
|
// SPECTRE — бухгалтерское ядро Fancy. Позиции клиента лежат в остатках OD_RESTS по регистру
|
|
// REG_3 = договор (D_B_CONTRACTS.DOC), в портфельном разделе плана счетов (OD_BALANCES.PART=39):
|
|
// деньги — счета 50/51/52/55 (остаток D в валюте VALUE_ID), бумаги — 58.xx (количество A,
|
|
// бумага REG_2), НКД — отдельный счёт 58.05 (не показываем: он в цене). OD_RESTS — текущий
|
|
// остаток; позиция на прошлую дату = остаток минус обороты OD_TURNS после этой даты
|
|
// (TYPE_ = +1 дебет / −1 кредит, что подтверждено сверкой: сумма оборотов = остаток).
|
|
// Оценка — по котировке OD_VALUES_RATES не старше 90 дней на дату (правило признаваемой
|
|
// котировки), иначе — балансовая стоимость EQ_D. Рецепт взят из InvestControl Hub
|
|
// (service.build_portfolio_base, deals_report) и проверен там на договорах ДУ-489 и БР-704.
|
|
//
|
|
// Операции — из проводок OD_WIRING по портфельным счетам договора: покупка, продажа, ввод и
|
|
// вывод бумаг, погашение, конвертация; выручка при продаже берётся со счёта ВЫРУЧКА.
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// spectreRecognizeDays — котировка старше этого срока не считается рыночной.
|
|
const spectreRecognizeDays = 90
|
|
|
|
type spectreRest struct {
|
|
id int64
|
|
numS, sysName string
|
|
num1 int
|
|
secID, curID int64
|
|
a, d, eqD float64
|
|
isin, secName, cur string
|
|
class int // OD_SHARES.CLASS: 1 акция, 2 облигация, 7 депозитарная расписка …
|
|
nominal float64
|
|
}
|
|
|
|
// lkArchivePortfolio — портфель на дату по SPECTRE. date — в местном времени, конец дня включительно.
|
|
func lkArchivePortfolio(ctx context.Context, s FancyMSSQLSettings, contract string, date time.Time) (*LKPortfolio, error) {
|
|
c, ok := spectreContractFor(ctx, s, contract)
|
|
if !ok {
|
|
return nil, fmt.Errorf("договор %s в брокерском учёте не найден", lkContractNumber(contract))
|
|
}
|
|
conn, err := spectreOpen(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
cctx, cancel := context.WithTimeout(ctx, 40*time.Second)
|
|
defer cancel()
|
|
day := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC)
|
|
// Мы читаем копию учёта; она может отставать. Позже её последнего дня данных нет —
|
|
// показываем портфель на последний день копии и говорим об этом.
|
|
note := ""
|
|
if last := spectreAsOf(cctx, conn); !last.IsZero() && day.After(last) {
|
|
day = last
|
|
note = "данные брокерского учёта доступны по " + last.Format("02.01.2006")
|
|
}
|
|
today := time.Now()
|
|
isPast := day.Before(time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.UTC))
|
|
|
|
rows, err := conn.QueryContext(cctx, fsql.Arch01, sql.Named("c", c.DOC))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("остатки: %v", err)
|
|
}
|
|
var rests []spectreRest
|
|
for rows.Next() {
|
|
var r spectreRest
|
|
if rows.Scan(&r.id, &r.numS, &r.sysName, &r.num1, &r.secID, &r.curID, &r.a, &r.d, &r.eqD,
|
|
&r.isin, &r.secName, &r.cur, &r.class, &r.nominal) == nil {
|
|
rests = append(rests, r)
|
|
}
|
|
}
|
|
rows.Close()
|
|
|
|
// Прошлая дата: откатываем остатки на обороты после конца этого дня.
|
|
if isPast && len(rests) > 0 {
|
|
ids := make([]string, 0, len(rests))
|
|
for _, r := range rests {
|
|
ids = append(ids, fmt.Sprint(r.id))
|
|
}
|
|
trows, err := conn.QueryContext(cctx, fmt.Sprintf(fsql.Arch02, strings.Join(ids, ",")), sql.Named("d", day.AddDate(0, 0, 1)))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("обороты: %v", err)
|
|
}
|
|
adj := map[int64][3]float64{}
|
|
for trows.Next() {
|
|
var id int64
|
|
var a, v, e float64
|
|
if trows.Scan(&id, &a, &v, &e) == nil {
|
|
adj[id] = [3]float64{a, v, e}
|
|
}
|
|
}
|
|
trows.Close()
|
|
for i := range rests {
|
|
if x, ok := adj[rests[i].id]; ok {
|
|
rests[i].a -= x[0]
|
|
rests[i].d -= x[1]
|
|
rests[i].eqD -= x[2]
|
|
}
|
|
}
|
|
}
|
|
|
|
out := &LKPortfolio{Source: "spectre", AsOf: day.Format("02.01.2006"), Note: note}
|
|
fx := spectreFx(cctx, conn, day)
|
|
byISIN := map[string]*LKPosition{}
|
|
cash := map[string]float64{}
|
|
for _, r := range rests {
|
|
isCash := r.num1 != 58
|
|
if isCash {
|
|
if abs(r.d) < 0.005 {
|
|
continue
|
|
}
|
|
cur := spectreCurCode(r.cur)
|
|
cash[cur] += r.d
|
|
if cur == "SUR" {
|
|
out.CashRUB += r.d
|
|
} else {
|
|
out.CashRUB += r.d * fx(r.curID)
|
|
}
|
|
continue
|
|
}
|
|
if strings.Contains(strings.ToUpper(r.sysName), "НКД") || strings.Contains(strings.ToUpper(r.sysName), "СКД") {
|
|
continue // купонный доход сидит в цене, отдельной строкой клиенту не нужен
|
|
}
|
|
if abs(r.a) < 1e-9 {
|
|
continue
|
|
}
|
|
key := r.isin
|
|
if key == "" {
|
|
key = fmt.Sprintf("id:%d", r.secID)
|
|
}
|
|
pos := byISIN[key]
|
|
if pos == nil {
|
|
pos = &LKPosition{ISIN: r.isin, Name: r.secName, Kind: spectreKind(r.class), Currency: spectreCurCode(r.cur)}
|
|
byISIN[key] = pos
|
|
}
|
|
pos.Quantity += r.a
|
|
pos.Cost += r.eqD
|
|
// Оценка: рыночная котировка на дату, если не старше 90 дней, иначе балансовая.
|
|
// Котировки OD_VALUES_RATES уже в рублях за одну бумагу (и для валютных выпусков —
|
|
// проверено: НОВАТЭК 001P-03 с номиналом 100 USD котируется как 7 967 ₽).
|
|
price, age := spectreQuote(cctx, conn, r.secID, day)
|
|
if price > 0 && age <= spectreRecognizeDays {
|
|
pos.Value += r.a * price
|
|
pos.Price = price
|
|
} else {
|
|
pos.Value += r.eqD
|
|
pos.Note = "оценка по балансовой стоимости: нет свежей котировки"
|
|
}
|
|
}
|
|
for _, p := range byISIN {
|
|
p.PnL = p.Value - p.Cost
|
|
if p.Cost != 0 {
|
|
p.PnLPct = p.PnL / p.Cost * 100
|
|
}
|
|
if p.Quantity != 0 && p.Cost != 0 {
|
|
p.AvgPrice = p.Cost / p.Quantity
|
|
}
|
|
out.Positions = append(out.Positions, *p)
|
|
}
|
|
for cur, amt := range cash {
|
|
if abs(amt) >= 0.005 {
|
|
out.Cash = append(out.Cash, LKCash{Currency: cur, Amount: amt})
|
|
}
|
|
}
|
|
sort.Slice(out.Cash, func(i, j int) bool { return out.Cash[i].Currency < out.Cash[j].Currency })
|
|
out.Priced = 0
|
|
for _, p := range out.Positions {
|
|
if p.Price > 0 {
|
|
out.Priced++
|
|
}
|
|
}
|
|
lkPortfolioTotals(out)
|
|
if c.Closed {
|
|
out.Note = strings.TrimSpace(strings.TrimPrefix(out.Note+"; договор закрыт", "; "))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// spectreAsOf — последний день, за который в копии учёта есть документы (кэш 10 минут).
|
|
var spectreAsOfCache struct {
|
|
mu sync.Mutex
|
|
at time.Time
|
|
v time.Time
|
|
}
|
|
|
|
func spectreAsOf(ctx context.Context, conn *sql.DB) time.Time {
|
|
spectreAsOfCache.mu.Lock()
|
|
defer spectreAsOfCache.mu.Unlock()
|
|
if time.Since(spectreAsOfCache.at) < 10*time.Minute {
|
|
return spectreAsOfCache.v
|
|
}
|
|
var d sql.NullTime
|
|
_ = conn.QueryRowContext(ctx, fsql.Arch03).Scan(&d)
|
|
v := time.Time{}
|
|
if d.Valid {
|
|
v = time.Date(d.Time.Year(), d.Time.Month(), d.Time.Day(), 0, 0, 0, 0, time.UTC)
|
|
}
|
|
spectreAsOfCache.at, spectreAsOfCache.v = time.Now(), v
|
|
return v
|
|
}
|
|
|
|
func abs(v float64) float64 {
|
|
if v < 0 {
|
|
return -v
|
|
}
|
|
return v
|
|
}
|
|
|
|
// spectreCurCode — код валюты в терминах кабинета (рубли — SUR, как в QUIK).
|
|
func spectreCurCode(sys string) string {
|
|
switch strings.ToUpper(strings.TrimSpace(sys)) {
|
|
case "RUB", "RUR", "SUR", "":
|
|
return "SUR"
|
|
}
|
|
return strings.ToUpper(strings.TrimSpace(sys))
|
|
}
|
|
|
|
// spectreKind — вид бумаги по классу OD_SHARES.CLASS.
|
|
func spectreKind(class int) string {
|
|
switch class {
|
|
case 1:
|
|
return "акция"
|
|
case 2:
|
|
return "облигация"
|
|
case 3, 4:
|
|
return "пай"
|
|
case 7:
|
|
return "депозитарная расписка"
|
|
}
|
|
return "бумага"
|
|
}
|
|
|
|
// spectreQuote — котировка бумаги на дату (не позже) и её возраст в днях.
|
|
func spectreQuote(ctx context.Context, conn *sql.DB, secID int64, day time.Time) (float64, int) {
|
|
var ofic sql.NullTime
|
|
var rate3, pfr, closeP sql.NullFloat64
|
|
err := conn.QueryRowContext(ctx, fsql.Arch04, sql.Named("v", secID), sql.Named("d", day.AddDate(0, 0, 1))).Scan(&ofic, &rate3, &pfr, &closeP)
|
|
if err != nil {
|
|
return 0, 1 << 30
|
|
}
|
|
price := pfr.Float64
|
|
if price <= 0 {
|
|
price = rate3.Float64
|
|
}
|
|
if price <= 0 {
|
|
price = closeP.Float64
|
|
}
|
|
age := 1 << 30
|
|
if ofic.Valid {
|
|
age = int(day.Sub(ofic.Time).Hours() / 24)
|
|
if age < 0 {
|
|
age = 0
|
|
}
|
|
}
|
|
return price, age
|
|
}
|
|
|
|
// spectreFx — курс валюты к рублю на дату (MARKET=0), с кэшем на запрос.
|
|
func spectreFx(ctx context.Context, conn *sql.DB, day time.Time) func(curID int64) float64 {
|
|
cache := map[int64]float64{}
|
|
return func(curID int64) float64 {
|
|
if curID == 0 {
|
|
return 1
|
|
}
|
|
if v, ok := cache[curID]; ok {
|
|
return v
|
|
}
|
|
var rate sql.NullFloat64
|
|
_ = conn.QueryRowContext(ctx, fsql.Arch05,
|
|
sql.Named("v", curID), sql.Named("d", day.AddDate(0, 0, 1))).Scan(&rate)
|
|
v := rate.Float64
|
|
if v <= 0 {
|
|
v = 1
|
|
}
|
|
cache[curID] = v
|
|
return v
|
|
}
|
|
}
|
|
|
|
// LKOperationRow — операция по брокерскому счёту для ленты кабинета.
|
|
type LKOperationRow struct {
|
|
Date string `json:"date"`
|
|
Kind string `json:"kind"` // покупка | продажа | ввод бумаг | вывод бумаг | погашение | конвертация | замещение | пополнение | вывод денег
|
|
Side string `json:"side"` // in | out
|
|
Name string `json:"name"` // бумага или «Денежные средства»
|
|
ISIN string `json:"isin,omitempty"`
|
|
Quantity float64 `json:"quantity,omitempty"`
|
|
Price float64 `json:"price,omitempty"` // цена за бумагу в валюте
|
|
Amount float64 `json:"amount,omitempty"` // сумма в валюте операции
|
|
Currency string `json:"currency,omitempty"`
|
|
AmountRUB float64 `json:"amount_rub"` // сумма в рублях
|
|
}
|
|
|
|
// lkArchiveOperations — сделки и движения по портфельным счетам договора за период.
|
|
// Лента операций за период не меняется в течение дня, а год данных из журнала
|
|
// проводок читается секундами: держим готовый ответ пять минут.
|
|
var opsCache = struct {
|
|
mu sync.Mutex
|
|
m map[string]opsCached
|
|
}{m: map[string]opsCached{}}
|
|
|
|
type opsCached struct {
|
|
at time.Time
|
|
rows []LKOperationRow
|
|
}
|
|
|
|
func lkArchiveOperations(ctx context.Context, s FancyMSSQLSettings, contract string, from, to time.Time) ([]LKOperationRow, error) {
|
|
key := lkContractNumber(contract) + "|" + from.Format("2006-01-02") + "|" + to.Format("2006-01-02")
|
|
opsCache.mu.Lock()
|
|
c, ok := opsCache.m[key]
|
|
opsCache.mu.Unlock()
|
|
if ok && time.Since(c.at) < 5*time.Minute {
|
|
return c.rows, nil
|
|
}
|
|
rows, err := lkArchiveOperationsLoad(ctx, s, contract, from, to)
|
|
if err == nil {
|
|
opsCache.mu.Lock()
|
|
opsCache.m[key] = opsCached{at: time.Now(), rows: rows}
|
|
opsCache.mu.Unlock()
|
|
}
|
|
return rows, err
|
|
}
|
|
|
|
func lkArchiveOperationsLoad(ctx context.Context, s FancyMSSQLSettings, contract string, from, to time.Time) ([]LKOperationRow, error) {
|
|
c, ok := spectreContractFor(ctx, s, contract)
|
|
if !ok {
|
|
return nil, fmt.Errorf("договор %s в брокерском учёте не найден", lkContractNumber(contract))
|
|
}
|
|
conn, err := spectreOpen(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
cctx, cancel := context.WithTimeout(ctx, 40*time.Second)
|
|
defer cancel()
|
|
// Счета бумаг (ПОРТФЕЛЬ) и денег (касса) договора.
|
|
rows, err := conn.QueryContext(cctx, fsql.Arch06, sql.Named("c", c.DOC))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var secIDs, cashIDs []string
|
|
secSet := map[int64]bool{}
|
|
for rows.Next() {
|
|
var id int64
|
|
var num1 int
|
|
var sys string
|
|
if rows.Scan(&id, &num1, &sys) == nil {
|
|
if num1 == 58 {
|
|
secIDs = append(secIDs, fmt.Sprint(id))
|
|
secSet[id] = true
|
|
} else {
|
|
cashIDs = append(cashIDs, fmt.Sprint(id))
|
|
}
|
|
}
|
|
}
|
|
rows.Close()
|
|
var out []LKOperationRow
|
|
toEnd := time.Date(to.Year(), to.Month(), to.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, 1)
|
|
fromDay := time.Date(from.Year(), from.Month(), from.Day(), 0, 0, 0, 0, time.UTC)
|
|
|
|
// Три обхода журнала проводок независимы, а каждый по году данных идёт секунды:
|
|
// выполняем их разом, иначе клиент ждёт их сумму.
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
var cashOut []LKOperationRow
|
|
var proceeds map[string][2]float64
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
proceeds = spectreProceeds(cctx, conn, c.DOC, fromDay, toEnd)
|
|
}()
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
cashOut = spectreCashOps(cctx, conn, cashIDs, fromDay, toEnd)
|
|
}()
|
|
|
|
if len(secIDs) > 0 {
|
|
in := strings.Join(secIDs, ",")
|
|
// Сторона проводки выбирается заранее двумя ветвями UNION ALL: связка
|
|
// «JOIN … ON sr.ID = CASE WHEN w.D_REST IN (…)» заставляла SQL Server
|
|
// разбирать список счетов на каждой строке журнала — год данных читался
|
|
// 6–7 секунд, сейчас — десятки миллисекунд (оба индекса по D_REST/K_REST
|
|
// с WIRDATE отрабатывают поиском).
|
|
wrows, err := conn.QueryContext(cctx, fmt.Sprintf(fsql.Arch07, in), sql.Named("f", fromDay), sql.Named("t", toEnd))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("проводки: %v", err)
|
|
}
|
|
type key struct{ day, kind, isin, cur string }
|
|
agg := map[key]*LKOperationRow{}
|
|
var order []key
|
|
for wrows.Next() {
|
|
var wd time.Time
|
|
var eq, dSum, kSum, dAmt, kAmt float64
|
|
var dRest, kRest int64
|
|
var wname, secName, isin, cur string
|
|
if wrows.Scan(&wd, &eq, &dRest, &kRest, &dSum, &kSum, &dAmt, &kAmt, &wname, &secName, &isin, &cur) != nil {
|
|
continue
|
|
}
|
|
onDebit := secSet[dRest]
|
|
kind := spectreDealKind(wname, onDebit)
|
|
if kind == "" {
|
|
continue
|
|
}
|
|
qty, sum := kAmt, dSum
|
|
if onDebit {
|
|
qty, sum = dAmt, kSum
|
|
}
|
|
k := key{wd.Format("2006-01-02"), kind, isin, spectreCurCode(cur)}
|
|
a := agg[k]
|
|
if a == nil {
|
|
side := "out"
|
|
if onDebit {
|
|
side = "in"
|
|
}
|
|
a = &LKOperationRow{Date: wd.Format("02.01.2006"), Kind: kind, Side: side, Name: secName, ISIN: isin, Currency: k.cur}
|
|
agg[k] = a
|
|
order = append(order, k)
|
|
}
|
|
a.Quantity += qty
|
|
a.Amount += sum
|
|
a.AmountRUB += eq
|
|
}
|
|
wrows.Close()
|
|
// При продаже счёт ПОРТФЕЛЬ списывает бумагу по себестоимости; фактическая выручка —
|
|
// на счёте ВЫРУЧКА. Клиенту показываем именно её.
|
|
wg.Wait()
|
|
for _, k := range order {
|
|
a := agg[k]
|
|
if a.Kind == "продажа" || a.Kind == "погашение" {
|
|
if p, ok := proceeds[k.isin+"|"+k.day]; ok && p[1] != 0 {
|
|
a.Amount, a.AmountRUB = p[0], p[1]
|
|
}
|
|
}
|
|
if a.Quantity != 0 && a.Amount != 0 {
|
|
a.Price = a.Amount / a.Quantity
|
|
}
|
|
out = append(out, *a)
|
|
}
|
|
}
|
|
wg.Wait()
|
|
mu.Lock()
|
|
out = append(out, cashOut...)
|
|
mu.Unlock()
|
|
sort.SliceStable(out, func(i, j int) bool {
|
|
di, _ := time.Parse("02.01.2006", out[i].Date)
|
|
dj, _ := time.Parse("02.01.2006", out[j].Date)
|
|
return di.After(dj)
|
|
})
|
|
return out, nil
|
|
}
|
|
|
|
// spectreCashOps — пополнения и выводы денег по кассовым счетам договора.
|
|
func spectreCashOps(ctx context.Context, conn *sql.DB, cashIDs []string, fromDay, toEnd time.Time) []LKOperationRow {
|
|
if len(cashIDs) == 0 {
|
|
return nil
|
|
}
|
|
var out []LKOperationRow
|
|
in := strings.Join(cashIDs, ",")
|
|
crows, err := conn.QueryContext(ctx, fmt.Sprintf(fsql.Arch08, in), sql.Named("f", fromDay), sql.Named("t", toEnd))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer crows.Close()
|
|
cashSet := map[string]bool{}
|
|
for _, id := range cashIDs {
|
|
cashSet[id] = true
|
|
}
|
|
for crows.Next() {
|
|
var wd time.Time
|
|
var eq, dSum, kSum float64
|
|
var dRest, kRest int64
|
|
var wname, cur string
|
|
if crows.Scan(&wd, &eq, &dRest, &kRest, &dSum, &kSum, &wname, &cur) != nil {
|
|
continue
|
|
}
|
|
low := strings.ToLower(wname)
|
|
var kind, side string
|
|
switch {
|
|
case strings.Contains(low, "ввод") && strings.Contains(low, "ден"), strings.Contains(low, "пополнен"), strings.Contains(low, "зачисление денеж"):
|
|
kind, side = "пополнение", "in"
|
|
case strings.Contains(low, "вывод") && strings.Contains(low, "ден"), strings.Contains(low, "списание денеж") && !strings.Contains(low, "комисс"):
|
|
kind, side = "вывод денег", "out"
|
|
default:
|
|
continue
|
|
}
|
|
sum := kSum
|
|
if cashSet[fmt.Sprint(dRest)] {
|
|
sum = dSum
|
|
}
|
|
out = append(out, LKOperationRow{Date: wd.Format("02.01.2006"), Kind: kind, Side: side, Name: "Денежные средства",
|
|
Amount: sum, Currency: spectreCurCode(cur), AmountRUB: eq})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// spectreDealKind — вид операции по названию проводки (рецепт хаба). Пусто — не показываем.
|
|
func spectreDealKind(name string, onDebit bool) string {
|
|
n := strings.ToLower(name)
|
|
switch {
|
|
case strings.Contains(n, "замещени") || strings.Contains(n, "обмен"):
|
|
return "замещение"
|
|
case strings.Contains(n, "конвертац") || strings.Contains(n, "слияни"):
|
|
return "конвертация"
|
|
case strings.Contains(n, "погашен"):
|
|
return "погашение"
|
|
case strings.Contains(n, "ввод") && strings.Contains(n, "цб"):
|
|
return "ввод бумаг"
|
|
case strings.Contains(n, "вывод") && strings.Contains(n, "цб"):
|
|
return "вывод бумаг"
|
|
case strings.Contains(n, "заём") || strings.Contains(n, "займ") || strings.Contains(n, "репо"):
|
|
return "" // РЕПО и займы клиенту в ленте не показываем
|
|
}
|
|
if onDebit {
|
|
return "покупка"
|
|
}
|
|
return "продажа"
|
|
}
|
|
|
|
// spectreProceeds — выручка от продаж по (ISIN|день): [сумма в валюте, сумма в рублях].
|
|
func spectreProceeds(ctx context.Context, conn *sql.DB, doc int64, from, to time.Time) map[string][2]float64 {
|
|
out := map[string][2]float64{}
|
|
rows, err := conn.QueryContext(ctx, fsql.Arch09, sql.Named("c", doc))
|
|
if err != nil {
|
|
return out
|
|
}
|
|
var ids []string
|
|
idSet := map[int64]bool{}
|
|
for rows.Next() {
|
|
var id int64
|
|
if rows.Scan(&id) == nil {
|
|
ids = append(ids, fmt.Sprint(id))
|
|
idSet[id] = true
|
|
}
|
|
}
|
|
rows.Close()
|
|
if len(ids) == 0 {
|
|
return out
|
|
}
|
|
in := strings.Join(ids, ",")
|
|
wrows, err := conn.QueryContext(ctx, fmt.Sprintf(fsql.Arch10, in), sql.Named("f", from), sql.Named("t", to))
|
|
if err != nil {
|
|
return out
|
|
}
|
|
defer wrows.Close()
|
|
for wrows.Next() {
|
|
var wd time.Time
|
|
var eq, dSum, kSum float64
|
|
var dRest, kRest int64
|
|
var isin string
|
|
if wrows.Scan(&wd, &eq, &dRest, &kRest, &dSum, &kSum, &isin) != nil {
|
|
continue
|
|
}
|
|
sum := dSum
|
|
if idSet[kRest] {
|
|
sum = kSum
|
|
}
|
|
k := isin + "|" + wd.Format("2006-01-02")
|
|
v := out[k]
|
|
v[0] += sum
|
|
v[1] += eq
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|