Кабинет клиента: портфель как у брокера — «Онлайн» по 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
+75
View File
@@ -7,6 +7,7 @@ import (
"io"
"log"
"net/http"
"sort"
"strings"
"time"
)
@@ -271,6 +272,80 @@ func registerLKAPI(mux *http.ServeMux, rc *RuntimeConfig, svc *Service) {
lkJSON(w, http.StatusOK, map[string]any{"operations": ops, "from": q.Get("from"), "to": q.Get("to")})
})
// Портфель клиента как у брокера: view=online — по экспорту QUIK (что есть сейчас), если
// в QUIK клиента нет — по брокерскому учёту на сегодня; view=archive&date=YYYY-MM-DD —
// портфель на дату по брокерскому учёту (SPECTRE).
mux.HandleFunc("/api/lk/portfolio2", func(w http.ResponseWriter, r *http.Request) {
if !auth(w, r) {
return
}
u, ok := current(w, r)
if !ok {
return
}
fancy := rc.Snapshot().Fancy
q := r.URL.Query()
var pf *LKPortfolio
var err error
switch q.Get("view") {
case "archive":
day := time.Now()
if d, e := time.ParseInLocation("2006-01-02", q.Get("date"), time.Local); e == nil {
day = d
}
pf, err = lkArchivePortfolio(r.Context(), fancy, u.Contract, day)
default:
pf, err = lkOnlinePortfolio(r.Context(), rc, u.Contract)
if err == nil && len(pf.Positions) == 0 && len(pf.Cash) == 0 {
// В торговой системе клиента нет (не торгует через QUIK) — текущий портфель из учёта.
if ar, e := lkArchivePortfolio(r.Context(), fancy, u.Contract, time.Now()); e == nil {
ar.Note = "по данным брокерского учёта на сегодня"
pf = ar
}
}
}
if err != nil {
lkJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
return
}
pf.Note = strings.TrimSpace(pf.Note)
lkJSON(w, http.StatusOK, map[string]any{"portfolio": pf, "contract": u.Contract, "full_name": u.FullName})
})
// Операции по брокерскому счёту: сделки и движения бумаг (SPECTRE) плюс выплаты дохода (учёт).
mux.HandleFunc("/api/lk/ops", func(w http.ResponseWriter, r *http.Request) {
if !auth(w, r) {
return
}
u, ok := current(w, r)
if !ok {
return
}
fancy := rc.Snapshot().Fancy
q := r.URL.Query()
fromT, toT := parseRange(q.Get("from"), q.Get("to"))
rows, err := lkArchiveOperations(r.Context(), fancy, u.Contract, fromT, toT)
if err != nil {
rows = nil // без брокерского учёта показываем хотя бы выплаты
}
if inc, _, e := lkIncome(r.Context(), fancy, u.Contract, q.Get("from"), q.Get("to")); e == nil {
for _, p := range inc {
rows = append(rows, LKOperationRow{Date: p.Date, Kind: p.Kind, Side: "in", Name: p.Security, ISIN: p.ISIN,
Quantity: p.Quantity, Amount: p.Paid, Currency: "SUR", AmountRUB: p.Paid})
}
}
sort.SliceStable(rows, func(i, j int) bool {
di, _ := time.Parse("02.01.2006", rows[i].Date)
dj, _ := time.Parse("02.01.2006", rows[j].Date)
return di.After(dj)
})
resp := map[string]any{"operations": rows, "from": fromT.Format("2006-01-02"), "to": toT.Format("2006-01-02")}
if err != nil {
resp["note"] = err.Error()
}
lkJSON(w, http.StatusOK, resp)
})
// Выплаты дохода и удержанный налог.
mux.HandleFunc("/api/lk/income", func(w http.ResponseWriter, r *http.Request) {
if !auth(w, r) {