Экспорт QUIK: настройка пакетами данных вместо правки export.ini
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -70,7 +70,8 @@ type QuikExportSettings struct {
|
|||||||
ROPassword string `json:"ro_password"`
|
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"`
|
Tables []string `json:"tables"`
|
||||||
PortfolioPeriod int `json:"portfolio_period"`
|
PortfolioPeriod int `json:"portfolio_period"`
|
||||||
ParamPeriod int `json:"param_period"`
|
ParamPeriod int `json:"param_period"`
|
||||||
@@ -112,8 +113,19 @@ func (s QuikExportSettings) withDefaults() QuikExportSettings {
|
|||||||
if s.Preset == "" {
|
if s.Preset == "" {
|
||||||
s.Preset = "npr"
|
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 {
|
if len(s.Tables) == 0 {
|
||||||
s.Tables = quikExportPresetTables(s.Preset)
|
s.Tables = []string{"Portfolio"}
|
||||||
}
|
}
|
||||||
if s.PortfolioPeriod == 0 {
|
if s.PortfolioPeriod == 0 {
|
||||||
s.PortfolioPeriod = 60
|
s.PortfolioPeriod = 60
|
||||||
@@ -210,6 +222,114 @@ var quikExportTables = []quikExportTable{
|
|||||||
{"SecAccIntEx", "Внутренние переводы бумаг", "Прочее", "низкий", ""},
|
{"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 }{
|
var quikExportPresets = []struct{ Key, Title, Desc string }{
|
||||||
{"npr", "Минимум — только НПР/УДС", "Portfolio. Достаточно для модуля «НПР / УДС». ≈20 МБ/сутки."},
|
{"npr", "Минимум — только НПР/УДС", "Portfolio. Достаточно для модуля «НПР / УДС». ≈20 МБ/сутки."},
|
||||||
{"depo", "Депозитарий — портфели, позиции, справочники", "Portfolio + DepoLimits, MoneyLimits, AccountPositions + справочники бумаг и счетов. Даёт bj дополнительные проверки при M2M (позиция клиента в QUIK). ≈50–100 МБ/сутки."},
|
{"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 {
|
if tabs := iniEnabledOutputTables(exp); len(tabs) > 0 {
|
||||||
s.Tables = tabs
|
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 != "" {
|
if inf != "" {
|
||||||
set(&s.QuikHost, iniGet(inf, "connect", "trade-server-ip-address"))
|
set(&s.QuikHost, iniGet(inf, "connect", "trade-server-ip-address"))
|
||||||
@@ -739,6 +863,7 @@ type quikExportDBStats struct {
|
|||||||
Name, Size string
|
Name, Size string
|
||||||
Rows int64
|
Rows int64
|
||||||
}
|
}
|
||||||
|
NonEmpty map[string]bool // таблицы экспортёра, в которых есть данные
|
||||||
HistoryRows int64
|
HistoryRows int64
|
||||||
HistoryFrom time.Time
|
HistoryFrom time.Time
|
||||||
Rows24h int64
|
Rows24h int64
|
||||||
@@ -828,6 +953,16 @@ func quikExportStats(s QuikExportSettings) quikExportDBStats {
|
|||||||
st.LastTick = *tick
|
st.LastTick = *tick
|
||||||
st.TickAgeSec = int(time.Since(*tick).Seconds())
|
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)
|
_ = p.QueryRow(ctx, `SELECT count(DISTINCT "ClientCode") FROM "Portfolio"`).Scan(&st.Clients)
|
||||||
st.OK = true
|
st.OK = true
|
||||||
return st
|
return st
|
||||||
@@ -850,6 +985,9 @@ type QuikExportView struct {
|
|||||||
Tables []quikExportTable
|
Tables []quikExportTable
|
||||||
Groups []string
|
Groups []string
|
||||||
Presets []struct{ Key, Title, Desc string }
|
Presets []struct{ Key, Title, Desc string }
|
||||||
|
Packages []quikExportPackage
|
||||||
|
PkgOn map[string]bool // пакет выбран в настройках
|
||||||
|
PkgLive map[string]bool // пакет уже выгружается экспортёром (по export.ini)
|
||||||
Enabled map[string]bool
|
Enabled map[string]bool
|
||||||
RODSN string
|
RODSN string
|
||||||
RODSNFull string
|
RODSNFull string
|
||||||
@@ -881,6 +1019,12 @@ func quikExportView(st *Store, licensed, showRO bool) QuikExportView {
|
|||||||
for _, t := range s.Tables {
|
for _, t := range s.Tables {
|
||||||
v.Enabled[t] = true
|
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{}
|
seen := map[string]bool{}
|
||||||
for _, t := range quikExportTables {
|
for _, t := range quikExportTables {
|
||||||
if !seen[t.Group] {
|
if !seen[t.Group] {
|
||||||
@@ -903,6 +1047,15 @@ func quikExportView(st *Store, licensed, showRO bool) QuikExportView {
|
|||||||
v.DiskDB = diskUsage("/var/lib/postgresql")
|
v.DiskDB = diskUsage("/var/lib/postgresql")
|
||||||
}
|
}
|
||||||
v.DB = quikExportStats(s)
|
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.LogTail = quikExportTail(filepath.Join(s.Dir, "logs", "export.log"), 12)
|
||||||
v.ErrTail = quikExportTail(filepath.Join(s.Dir, "logs", "export.err"), 6)
|
v.ErrTail = quikExportTail(filepath.Join(s.Dir, "logs", "export.err"), 6)
|
||||||
v.RODSN = s.RODSN(false)
|
v.RODSN = s.RODSN(false)
|
||||||
@@ -1023,8 +1176,8 @@ func (srv *Server) saveQuikExport(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.ROPassword = p
|
s.ROPassword = p
|
||||||
}
|
}
|
||||||
case "volume":
|
case "volume":
|
||||||
s.Preset = f("preset")
|
if f("mode") == "custom" {
|
||||||
if s.Preset == "custom" {
|
s.Preset = "custom"
|
||||||
s.Tables = nil
|
s.Tables = nil
|
||||||
for _, t := range r.Form["tables"] {
|
for _, t := range r.Form["tables"] {
|
||||||
s.Tables = append(s.Tables, t)
|
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 {
|
if len(s.Tables) == 0 {
|
||||||
s.Tables = []string{"Portfolio"}
|
s.Tables = []string{"Portfolio"}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
s.Tables = quikExportPresetTables(s.Preset)
|
|
||||||
}
|
|
||||||
sort.Strings(s.Tables)
|
sort.Strings(s.Tables)
|
||||||
|
s.Packages = quikExportPackagesOf(s.Tables)
|
||||||
|
} else {
|
||||||
|
s.Preset = "packages"
|
||||||
|
s.Packages = append([]string(nil), r.Form["packages"]...)
|
||||||
|
s.Tables = quikExportPackageTables(s.Packages)
|
||||||
|
}
|
||||||
s.PortfolioPeriod = atoi(f("portfolio_period"), 60)
|
s.PortfolioPeriod = atoi(f("portfolio_period"), 60)
|
||||||
if s.PortfolioPeriod < 10 {
|
if s.PortfolioPeriod < 10 {
|
||||||
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")
|
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))
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package quikexport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Пакеты: набор таблиц, обратное определение пакетов, обязательный Portfolio.
|
||||||
|
func TestQuikExportPackages(t *testing.T) {
|
||||||
|
tabs := quikExportPackageTables([]string{"positions", "refs"})
|
||||||
|
for _, want := range []string{"Portfolio", "DepoLimits", "MoneyLimits", "Securities", "SecDepoAccount"} {
|
||||||
|
if !slices.Contains(tabs, want) {
|
||||||
|
t.Fatalf("нет %s в %v", want, tabs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if slices.Contains(tabs, "AllTrades") {
|
||||||
|
t.Fatalf("лишняя таблица рынка: %v", tabs)
|
||||||
|
}
|
||||||
|
if got := quikExportPackageTables(nil); len(got) != 1 || got[0] != "Portfolio" {
|
||||||
|
t.Fatalf("без пакетов остаётся только Portfolio: %v", got)
|
||||||
|
}
|
||||||
|
pk := quikExportPackagesOf(tabs)
|
||||||
|
if !slices.Contains(pk, "positions") || !slices.Contains(pk, "refs") || !slices.Contains(pk, "risk") {
|
||||||
|
t.Fatalf("обратное определение: %v", pk)
|
||||||
|
}
|
||||||
|
if slices.Contains(pk, "deals") {
|
||||||
|
t.Fatalf("пакет сделок не выбран: %v", pk)
|
||||||
|
}
|
||||||
|
// Каждая таблица пакета есть в каталоге экспортёра.
|
||||||
|
known := map[string]bool{}
|
||||||
|
for _, tb := range quikExportTables {
|
||||||
|
known[tb.Name] = true
|
||||||
|
}
|
||||||
|
for _, p := range quikExportPackages {
|
||||||
|
for _, tb := range p.Tables {
|
||||||
|
if !known[tb] {
|
||||||
|
t.Errorf("пакет %s: таблицы %s нет в каталоге", p.Key, tb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Старая настройка (preset=depo) переносится в пакеты.
|
||||||
|
func TestQuikExportPresetMigration(t *testing.T) {
|
||||||
|
s := QuikExportSettings{Preset: "depo"}.withDefaults()
|
||||||
|
if s.Preset != "packages" || len(s.Packages) == 0 {
|
||||||
|
t.Fatalf("миграция набора: %+v", s.Packages)
|
||||||
|
}
|
||||||
|
if !slices.Contains(s.Tables, "DepoLimits") || !slices.Contains(s.Tables, "Portfolio") {
|
||||||
|
t.Fatalf("таблицы после миграции: %v", s.Tables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Раздел настроек рисует пакеты и сохраняет выбор.
|
||||||
|
func TestQuikExportPackagesSection(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
keyPath := filepath.Join(dir, "module.key")
|
||||||
|
if err := os.WriteFile(keyPath, []byte(testKey+"\n"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
srv, err := New(Config{StorePath: filepath.Join(dir, "settings.json"), SetupPath: filepath.Join(dir, "setup.json"), KeyPath: keyPath})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
h := srv.Handler()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/section", nil)
|
||||||
|
req.Header.Set("X-BJ-Module-Key", testKey)
|
||||||
|
req.Header.Set("X-BJ-Licensed", "1")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("раздел: %d %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
for _, want := range []string{"Какие данные выгружаем", "Позиции и деньги клиента", "Цены для оценки портфеля", "Личный кабинет"} {
|
||||||
|
if !strings.Contains(rec.Body.String(), want) {
|
||||||
|
t.Errorf("в разделе нет %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form := url.Values{"part": {"volume"}, "mode": {"packages"}, "packages": {"risk", "positions", "prices"},
|
||||||
|
"portfolio_period": {"60"}, "param_period": {"30"}, "limit_kinds": {"0, 1, 365"}, "debug_level": {"1"}}
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/admin/setup/quikexport", strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Header.Set("X-BJ-Module-Key", testKey)
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusNoContent {
|
||||||
|
t.Fatalf("сохранение: %d", rec.Code)
|
||||||
|
}
|
||||||
|
s := srv.store.Get()
|
||||||
|
if s.Preset != "packages" || !slices.Contains(s.Tables, "DepoLimits") || !slices.Contains(s.Tables, "Params") {
|
||||||
|
t.Fatalf("после сохранения: %v %v", s.Preset, s.Tables)
|
||||||
|
}
|
||||||
|
if slices.Contains(s.Tables, "Trades") {
|
||||||
|
t.Fatalf("пакет сделок не выбирали: %v", s.Tables)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,27 +96,54 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2><span class="dot ok"></span>3. Объём данных</h2>
|
<h2><span class="dot ok"></span>3. Какие данные выгружаем</h2>
|
||||||
<p class="muted">Какие таблицы QUIK выгружаем. Чем больше набор, тем больше нагрузка на сервер QUIK, база и диск. Портфели пересчитываются каждые «период портфелей» секунд (ARQA рекомендует не менее 60); история изменений НПР/УДС хранится в схеме <code>npr</code>.</p>
|
<p class="muted">Выгрузка настраивается пакетами: отметьте, что нужно, — bj сам соберёт секцию <code>[Output tables]</code> в <code>export.ini</code>, править файл руками не нужно. Чем больше пакетов, тем выше нагрузка на сервер QUIK, базу и диск. Пакет «Маржа и риск» включён всегда: на нём держится модуль «НПР / УДС».</p>
|
||||||
<form method="post" action="/admin/setup/quikexport" style="display:grid;gap:10px;margin-top:10px">
|
<form method="post" action="/admin/setup/quikexport" style="display:grid;gap:12px;margin-top:10px">
|
||||||
<input type="hidden" name="section" value="sec-quikexport"><input type="hidden" name="part" value="volume">
|
<input type="hidden" name="section" value="sec-quikexport"><input type="hidden" name="part" value="volume">
|
||||||
<div style="display:grid;gap:6px">{{range $X.Presets}}
|
<input type="hidden" name="mode" value="{{if eq $S.Preset "custom"}}custom{{else}}packages{{end}}" id="qx-mode">
|
||||||
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:400;margin:0"><input type="radio" name="preset" value="{{.Key}}" {{if eq $S.Preset .Key}}checked{{end}} onchange="document.getElementById('qx-tables').style.display=this.value==='custom'?'block':'none'"><span><b>{{.Title}}</b><br><span class="muted" style="font-size:12px">{{.Desc}}</span></span></label>{{end}}
|
<div id="qx-packages" style="display:{{if eq $S.Preset "custom"}}none{{else}}grid{{end}};grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:10px">
|
||||||
|
{{range $X.Packages}}
|
||||||
|
<label style="display:block;border:1px solid {{if index $X.PkgOn .Key}}var(--accent){{else}}var(--border){{end}};border-radius:10px;padding:12px 14px;margin:0;font-weight:400;cursor:pointer;background:var(--card)">
|
||||||
|
<span style="display:flex;gap:8px;align-items:flex-start">
|
||||||
|
<input type="checkbox" name="packages" value="{{.Key}}" {{if index $X.PkgOn .Key}}checked{{end}} {{if eq .Key "risk"}}checked disabled{{end}} style="width:auto;margin-top:3px">
|
||||||
|
<span style="flex:1">
|
||||||
|
<b style="font-size:14px">{{.Title}}</b>
|
||||||
|
{{if index $X.PkgLive .Key}}<span class="badge ok" style="margin-left:6px" title="данные этого пакета уже есть в базе">данные поступают</span>{{else if index $X.PkgOn .Key}}<span class="badge warn" style="margin-left:6px" title="выбрано, но экспортёр ещё не перенастроен">после применения</span>{{end}}
|
||||||
|
<span class="muted" style="display:block;font-size:12.5px;margin-top:4px">{{.What}}</span>
|
||||||
|
<span style="display:block;margin-top:6px;font-size:12.5px">Что это даёт:</span>
|
||||||
|
<ul style="margin:2px 0 0;padding-left:18px;font-size:12.5px">{{range .Gives}}<li>{{.}}</li>{{end}}</ul>
|
||||||
|
<span class="muted" style="display:block;font-size:12px;margin-top:6px">Объём: {{.Load}} · таблиц: {{len .Tables}}</span>
|
||||||
|
{{if .Warn}}<span style="display:block;font-size:12px;margin-top:4px;color:var(--warn)">{{.Warn}}</span>{{end}}
|
||||||
|
<span class="muted" style="display:block;font-size:11.5px;margin-top:4px">{{range .Tables}}<code>{{.}}</code> {{end}}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>{{end}}
|
||||||
|
<input type="hidden" name="packages" value="risk">
|
||||||
</div>
|
</div>
|
||||||
<div id="qx-tables" style="display:{{if eq $S.Preset "custom"}}block{{else}}none{{end}};border:1px solid var(--border);border-radius:8px;padding:10px">
|
<div id="qx-tables" style="display:{{if eq $S.Preset "custom"}}block{{else}}none{{end}};border:1px solid var(--border);border-radius:8px;padding:10px">
|
||||||
|
<p class="muted" style="margin:0 0 8px;font-size:12.5px">Ручной набор таблиц — когда пакетов недостаточно. Список таблиц экспортёра ARQA целиком.</p>
|
||||||
{{range $g := $X.Groups}}<div style="font-weight:600;margin:6px 0 4px">{{$g}}</div>
|
{{range $g := $X.Groups}}<div style="font-weight:600;margin:6px 0 4px">{{$g}}</div>
|
||||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:4px 12px">{{range $X.Tables}}{{if eq .Group $g}}
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:4px 12px">{{range $X.Tables}}{{if eq .Group $g}}
|
||||||
<label style="display:flex;gap:6px;align-items:flex-start;font-weight:400;margin:0;font-size:12.5px"><input type="checkbox" name="tables" value="{{.Name}}" {{if index $X.Enabled .Name}}checked{{end}}><span><code>{{.Name}}</code> — {{.Title}} <span class="muted">· объём: {{.Load}}{{if .Note}} · {{.Note}}{{end}}</span></span></label>{{end}}{{end}}</div>{{end}}
|
<label style="display:flex;gap:6px;align-items:flex-start;font-weight:400;margin:0;font-size:12.5px"><input type="checkbox" name="tables" value="{{.Name}}" {{if index $X.Enabled .Name}}checked{{end}} style="width:auto"><span><code>{{.Name}}</code> — {{.Title}} <span class="muted">· объём: {{.Load}}{{if .Note}} · {{.Note}}{{end}}</span></span></label>{{end}}{{end}}</div>{{end}}
|
||||||
</div>
|
</div>
|
||||||
<details><summary class="muted" style="cursor:pointer">Сейчас включено: {{len $S.Tables}} таблиц — {{range $S.Tables}}<code>{{.}}</code> {{end}}</summary></details>
|
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
|
||||||
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px;max-width:820px">
|
<button type="submit" class="btn btn-ok">Сохранить</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="qxMode()">{{if eq $S.Preset "custom"}}← Вернуться к пакетам{{else}}Выбрать таблицы вручную →{{end}}</button>
|
||||||
|
<span class="muted" style="font-size:12.5px">Сейчас выбрано таблиц: {{len $S.Tables}}{{if eq $S.Preset "custom"}} (ручной набор){{end}}. В экспортёр попадут после «Применить настройки».</span>
|
||||||
|
</div>
|
||||||
|
<details><summary class="muted" style="cursor:pointer;font-size:12.5px">Список выбранных таблиц</summary><p class="muted" style="font-size:12px">{{range $S.Tables}}<code>{{.}}</code> {{end}}</p></details>
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(4,minmax(140px,1fr));gap:10px;max-width:820px">
|
||||||
<label>Период портфелей, с <input type="number" name="portfolio_period" min="10" value="{{$S.PortfolioPeriod}}" style="width:100%"></label>
|
<label>Период портфелей, с <input type="number" name="portfolio_period" min="10" value="{{$S.PortfolioPeriod}}" style="width:100%"></label>
|
||||||
<label>Период «Текущих торгов», с <input type="number" name="param_period" min="0" value="{{$S.ParamPeriod}}" style="width:100%"></label>
|
<label>Период «Текущих торгов», с <input type="number" name="param_period" min="0" value="{{$S.ParamPeriod}}" style="width:100%"></label>
|
||||||
<label>Виды лимитов (LimitKinds) <input type="text" name="limit_kinds" value="{{$S.LimitKinds}}" style="width:100%"></label>
|
<label>Виды лимитов (LimitKinds) <input type="text" name="limit_kinds" value="{{$S.LimitKinds}}" style="width:100%"></label>
|
||||||
<label>Уровень лога (DebugLevel) <input type="number" name="debug_level" min="0" max="255" value="{{$S.DebugLevel}}" style="width:100%"></label>
|
<label>Уровень лога (DebugLevel) <input type="number" name="debug_level" min="0" max="255" value="{{$S.DebugLevel}}" style="width:100%"></label>
|
||||||
</div>
|
</div>
|
||||||
<div><button type="submit" class="btn btn-ok">Сохранить</button></div>
|
|
||||||
</form>
|
</form>
|
||||||
|
<script>
|
||||||
|
function qxMode(){var m=document.getElementById('qx-mode'),p=document.getElementById('qx-packages'),t=document.getElementById('qx-tables');
|
||||||
|
var custom=m.value!=='custom';m.value=custom?'custom':'packages';p.style.display=custom?'none':'grid';t.style.display=custom?'block':'none';
|
||||||
|
event.target.textContent=custom?'← Вернуться к пакетам':'Выбрать таблицы вручную →'}
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
Reference in New Issue
Block a user