Экспорт QUIK: настройка пакетами данных вместо правки export.ini

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-17 21:43:41 +03:00
parent 1fbc5d2375
commit 893e1222ab
3 changed files with 318 additions and 16 deletions
+180 -7
View File
@@ -70,7 +70,8 @@ type QuikExportSettings struct {
ROPassword string `json:"ro_password"`
// Объём данных.
Preset string `json:"preset"` // npr | depo | trading | full | custom
Preset string `json:"preset"` // packages — набор пакетов; custom — таблицы вручную (npr|depo|trading|full — старые наборы)
Packages []string `json:"packages"` // выбранные пакеты данных
Tables []string `json:"tables"`
PortfolioPeriod int `json:"portfolio_period"`
ParamPeriod int `json:"param_period"`
@@ -112,8 +113,19 @@ func (s QuikExportSettings) withDefaults() QuikExportSettings {
if s.Preset == "" {
s.Preset = "npr"
}
if s.Preset != "custom" { // старые наборы (npr/depo/trading/full) переводим в пакеты
if len(s.Packages) == 0 {
if len(s.Tables) > 0 {
s.Packages = quikExportPackagesOf(s.Tables)
} else {
s.Packages = quikExportPackagesOf(quikExportPresetTables(s.Preset))
}
}
s.Preset = "packages"
s.Tables = quikExportPackageTables(s.Packages)
}
if len(s.Tables) == 0 {
s.Tables = quikExportPresetTables(s.Preset)
s.Tables = []string{"Portfolio"}
}
if s.PortfolioPeriod == 0 {
s.PortfolioPeriod = 60
@@ -210,6 +222,114 @@ var quikExportTables = []quikExportTable{
{"SecAccIntEx", "Внутренние переводы бумаг", "Прочее", "низкий", ""},
}
// quikExportPackage — «пакет данных»: набор таблиц экспортёра под конкретную задачу. Оператор
// отмечает пакеты галочками, bj сам собирает из них секцию [Output tables] в export.ini.
type quikExportPackage struct {
Key string
Title string
What string // какие данные приходят
Gives []string // что это даёт: кому и для чего
Load string // объём
Warn string // предупреждение (пусто — нет)
Tables []string
}
var quikExportPackages = []quikExportPackage{
{"risk", "Маржа и риск клиента", "Стоимость портфеля, начальная и минимальная маржа, скорректированная маржа, НПР1/НПР2, уровень достаточности средств (УДС), статус расчёта.",
[]string{
"Модуль «НПР / УДС»: списки клиентов, зоны риска, история УДС — без пакета модуль пуст",
"Личный кабинет: показатель достаточности средств и предупреждение о марджин-колле",
"Оповещения: выход клиента в зону риска",
}, "≈20 МБ/сутки при 5 тыс. клиентов и периоде 60 с", "", []string{"Portfolio"}},
{"positions", "Позиции и деньги клиента", "Остатки бумаг по торговым счетам (лимиты ДЕПО), денежные лимиты и позиции по счетам.",
[]string{
"Личный кабинет: портфель клиента по торговым счетам — что есть и сколько заблокировано",
"M2M: проверка позиции в торговой системе перед списанием бумаг и сверка с остатком депозитария",
"Бэк-офис: расхождения между учётом депозитария и торговой системой",
}, "средний — десятки МБ/сутки", "", []string{"DepoLimits", "MoneyLimits", "AccountPositions"}},
{"refs", "Справочники бумаг и счетов", "Инструменты и их коды, классы, фирмы, торговые счета, счета депо, валюты, коды расчётов, календари торгов.",
[]string{
"Сопоставление ISIN с кодом бумаги в QUIK — без этого позиции не связать с депозитарными",
"Личный кабинет: нормальные наименования бумаг вместо кодов",
"Привязка клиента к его торговому счёту и счёту депо",
}, "низкий — единицы МБ", "", []string{"Classes", "Securities", "SecuritiesDictionary", "Firms", "Trdaccs", "FirmDepoAccount", "SecDepoAccount", "CurrencyInfo", "SettleCodes", "Calendars"}},
{"prices", "Цены для оценки портфеля", "Текущие торги: последние цены, изменение к закрытию, параметры инструментов.",
[]string{
"Личный кабинет: стоимость портфеля и позиций в рублях, изменение за день",
"Отчёты и выписки с оценкой по рыночной цене",
}, "высокий — зависит от числа инструментов и периода «Текущих торгов»", "", []string{"Params"}},
{"deals", "Сделки и заявки клиента", "Сделки, заявки, стоп-заявки, внебиржевые сделки и отчёты по ним, результаты транзакций.",
[]string{
"Личный кабинет: история сделок и заявок клиента",
"Отчёты брокера и сверка операций",
}, "высокий — сотни МБ в активные дни", "", []string{"Trades", "Orders", "StopOrders", "NegDeals", "NegReports", "UsTrades", "TransResult"}},
{"futures", "Срочный рынок", "Позиции и лимиты на срочном рынке (FORTS).",
[]string{
"Личный кабинет и НПР: показатели клиентов, торгующих фьючерсами",
}, "средний", "", []string{"FuturesHolding", "FuturesLimits"}},
{"journals", "Журналы изменений лимитов", "История изменения денежных лимитов, лимитов по бумагам и позиций срочного рынка.",
[]string{
"Разбор спорных ситуаций: когда и на сколько изменилась позиция клиента",
"Аудит и внутренний контроль",
}, "высокий — растёт постоянно", "Журналы не очищаются экспортёром: следите за местом на диске.", []string{"MoneyLimitsJournal", "DepoLimitsJournal", "FuturesHoldingJournal"}},
{"ncc", "Риск-параметры НКЦ", "Обеспечение, ценовые коридоры, индикативные курсы, спреды, статистика маркет-мейкера.",
[]string{
"Риск-менеджмент и отчётность по требованиям НКЦ",
"Проверка ценовых коридоров при оценке обеспечения",
}, "низкий — средний", "", []string{"CCPHold", "CCPIndicative", "CCPPriceRange", "RmHold", "RmIndicative", "RmPriceRange", "RmPriceRangeFirm", "RmProductSpreadCurr", "MarketMakerStats", "MarketMakerForts", "MarketMakerExtStats"}},
{"market", "Весь рынок: обезличенные сделки и стаканы", "Все сделки рынка и котировки (стаканы) по инструментам.",
[]string{
"Аналитика и собственные расчёты по рынку",
}, "очень высокий — гигабайты в сутки", "Включайте только при явной необходимости и запасе места на диске: нагрузка на сервер QUIK и базу возрастает в разы.", []string{"AllTrades", "Quotes"}},
{"misc", "Прочее", "Внутренние переводы бумаг, переводы НКЦ, запреты, алгоритмические заявки.",
[]string{
"Редкие задачи бэк-офиса и расследования",
}, "низкий", "", []string{"SecAccIntEx", "NCCTransfers", "Prohibitions", "AlgoOrders", "AlgoLinkedOrders", "AlgoLinkedTrades"}},
}
// quikExportPackageTables — таблицы выбранных пакетов (Portfolio входит всегда: на нём держится НПР).
func quikExportPackageTables(keys []string) []string {
on := map[string]bool{"Portfolio": true}
for _, k := range keys {
for _, p := range quikExportPackages {
if p.Key == k {
for _, t := range p.Tables {
on[t] = true
}
}
}
}
out := make([]string, 0, len(on))
for _, t := range quikExportTables { // порядок каталога
if on[t.Name] {
out = append(out, t.Name)
}
}
return out
}
// quikExportPackagesOf — какие пакеты покрыты набором таблиц (все таблицы пакета включены).
func quikExportPackagesOf(tables []string) []string {
on := map[string]bool{}
for _, t := range tables {
on[t] = true
}
var out []string
for _, p := range quikExportPackages {
all := true
for _, t := range p.Tables {
if !on[t] {
all = false
break
}
}
if all {
out = append(out, p.Key)
}
}
return out
}
var quikExportPresets = []struct{ Key, Title, Desc string }{
{"npr", "Минимум — только НПР/УДС", "Portfolio. Достаточно для модуля «НПР / УДС». ≈20 МБ/сутки."},
{"depo", "Депозитарий — портфели, позиции, справочники", "Portfolio + DepoLimits, MoneyLimits, AccountPositions + справочники бумаг и счетов. Даёт bj дополнительные проверки при M2M (позиция клиента в QUIK). ≈50–100 МБ/сутки."},
@@ -553,7 +673,11 @@ func quikExportImport(ctx context.Context, st *Store) (string, error) {
}
if tabs := iniEnabledOutputTables(exp); len(tabs) > 0 {
s.Tables = tabs
s.Preset = quikExportDetectPreset(tabs)
s.Packages = quikExportPackagesOf(tabs)
s.Preset = "custom" // набор из файла: пакетами описывается, только если совпадает точно
if same(tabs, quikExportPackageTables(s.Packages)) {
s.Preset = "packages"
}
}
if inf != "" {
set(&s.QuikHost, iniGet(inf, "connect", "trade-server-ip-address"))
@@ -739,6 +863,7 @@ type quikExportDBStats struct {
Name, Size string
Rows int64
}
NonEmpty map[string]bool // таблицы экспортёра, в которых есть данные
HistoryRows int64
HistoryFrom time.Time
Rows24h int64
@@ -828,6 +953,16 @@ func quikExportStats(s QuikExportSettings) quikExportDBStats {
st.LastTick = *tick
st.TickAgeSec = int(time.Since(*tick).Seconds())
}
st.NonEmpty = map[string]bool{} // по каким таблицам экспортёр реально приносит данные
if rows, err := p.Query(ctx, `SELECT relname FROM pg_stat_user_tables WHERE schemaname='public' AND (n_live_tup > 0 OR reltuples > 0)`); err == nil {
for rows.Next() {
var n string
if rows.Scan(&n) == nil {
st.NonEmpty[n] = true
}
}
rows.Close()
}
_ = p.QueryRow(ctx, `SELECT count(DISTINCT "ClientCode") FROM "Portfolio"`).Scan(&st.Clients)
st.OK = true
return st
@@ -850,6 +985,9 @@ type QuikExportView struct {
Tables []quikExportTable
Groups []string
Presets []struct{ Key, Title, Desc string }
Packages []quikExportPackage
PkgOn map[string]bool // пакет выбран в настройках
PkgLive map[string]bool // пакет уже выгружается экспортёром (по export.ini)
Enabled map[string]bool
RODSN string
RODSNFull string
@@ -881,6 +1019,12 @@ func quikExportView(st *Store, licensed, showRO bool) QuikExportView {
for _, t := range s.Tables {
v.Enabled[t] = true
}
v.Packages = quikExportPackages
v.PkgOn = map[string]bool{}
for _, k := range s.Packages {
v.PkgOn[k] = true
}
v.PkgLive = map[string]bool{} // по каким пакетам данные уже есть в базе
seen := map[string]bool{}
for _, t := range quikExportTables {
if !seen[t.Group] {
@@ -903,6 +1047,15 @@ func quikExportView(st *Store, licensed, showRO bool) QuikExportView {
v.DiskDB = diskUsage("/var/lib/postgresql")
}
v.DB = quikExportStats(s)
if len(v.DB.NonEmpty) > 0 { // пакет отмечаем «выгружается», когда его таблицы в базе не пусты
var have []string
for t := range v.DB.NonEmpty {
have = append(have, t)
}
for _, k := range quikExportPackagesOf(have) {
v.PkgLive[k] = true
}
}
v.LogTail = quikExportTail(filepath.Join(s.Dir, "logs", "export.log"), 12)
v.ErrTail = quikExportTail(filepath.Join(s.Dir, "logs", "export.err"), 6)
v.RODSN = s.RODSN(false)
@@ -1023,8 +1176,8 @@ func (srv *Server) saveQuikExport(w http.ResponseWriter, r *http.Request) {
s.ROPassword = p
}
case "volume":
s.Preset = f("preset")
if s.Preset == "custom" {
if f("mode") == "custom" {
s.Preset = "custom"
s.Tables = nil
for _, t := range r.Form["tables"] {
s.Tables = append(s.Tables, t)
@@ -1032,10 +1185,13 @@ func (srv *Server) saveQuikExport(w http.ResponseWriter, r *http.Request) {
if len(s.Tables) == 0 {
s.Tables = []string{"Portfolio"}
}
sort.Strings(s.Tables)
s.Packages = quikExportPackagesOf(s.Tables)
} else {
s.Tables = quikExportPresetTables(s.Preset)
s.Preset = "packages"
s.Packages = append([]string(nil), r.Form["packages"]...)
s.Tables = quikExportPackageTables(s.Packages)
}
sort.Strings(s.Tables)
s.PortfolioPeriod = atoi(f("portfolio_period"), 60)
if s.PortfolioPeriod < 10 {
s.PortfolioPeriod = 10
@@ -1231,3 +1387,20 @@ func (srv *Server) previewQuikExport(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "===== info.ini =====\n%s\n\n===== /etc/odbc.ini (секция) =====\n%s\n\n===== export.ini =====\n%s", mask(inf), quikExportRenderODBC(s), mask(exp))
}
// same — одинаковые ли наборы имён таблиц.
func same(a, b []string) bool {
if len(a) != len(b) {
return false
}
m := map[string]bool{}
for _, v := range a {
m[v] = true
}
for _, v := range b {
if !m[v] {
return false
}
}
return true
}