Files
Bridge-and-Join-s/internal/lkgateway/web/quikexport/npr_migration.sql
T
zuevav da12a9ec1c feat(bj): модуль ЛК на PostgreSQL, обмен с DEPO, боевой ЛК с имитатором НРД, QUIK, НПР
- bj_exchange и «кран» Fancy убраны: данные модуля ЛК в PostgreSQL (схема lk)
- трансферные таблицы FTV_M2M: создание при установке, события, справочник участников
- сверка с DEPO: счёт, ФИО, дата рождения, ИИС по договору, квал-инвестор из опции DEPO
- боевой ЛК ЕСИА Финанс при тестовом ИШ: журнал, отказ через DEPO, статусы в ЛК
- онлайн-лицензирование в bj, план тестирования M2M для контрагента (интерактивный HTML)
- модули QUIK МНП, экспорт QUIK, НПР/УДС; документы и тестовые материалы

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 19:41:12 +03:00

251 lines
12 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- Миграция: схема npr с историей НПР1/НПР2/УДС через триггер на "Portfolio".
--
-- Применять ПОСЛЕ того, как таблицы QUIK-экспортёра развёрнуты штатным скриптом
-- quikexport_11/scripts/postgre/exporter_pg.sql
-- в ту же БД. В эталонной "Portfolio" уже есть все нужные колонки
-- (PortfolioValue, InitMargin, MinMargin, CorrectedMargin, FundsLevel, Status, RiskLevel),
-- поэтому отдельного ALTER TABLE не требуется.
--
-- Политика записи: триггер пишет в npr.history только когда
-- (а) клиент появился впервые, или
-- (б) сменилась «зона» УДС (CALL → WARN → OK → INF), или
-- (в) прошло более 30 мин с последней записи по этому клиенту (heartbeat).
-- Это резко сокращает объём истории — фиксируются моменты смены состояния,
-- а не каждый тик экспортёра. Дополняется политикой хранения (retention).
BEGIN;
-- (б) Схема для истории НПР/УДС.
CREATE SCHEMA IF NOT EXISTS npr;
-- (в) Таблица истории. Append-only: свой BIGSERIAL PK, без UNIQUE по клиенту.
CREATE TABLE IF NOT EXISTS npr.history (
id BIGSERIAL PRIMARY KEY,
ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
firm_id varchar(12) NOT NULL,
client_code varchar(12) NOT NULL,
limit_kind varchar(5) NOT NULL,
curr_code varchar(4) NOT NULL,
margin_type varchar(4) NULL,
portfolio_value decimal(28, 8) NULL,
init_margin float NULL,
min_margin float NULL,
corrected_margin float NULL,
rcv1 float NULL,
rcv2 float NULL,
funds_level decimal(19, 2) NULL,
status int NULL,
risk_level int NULL
);
-- (г) Индекс для выборок «история по клиенту за период».
CREATE INDEX IF NOT EXISTS ix_npr_hist_client_ts
ON npr.history (client_code, ts DESC);
-- «Пульс» от экспортёра: триггер обновляет last_tick на каждом тике (не чаще раза в 10 сек).
-- Нужен отдельно от истории, чтобы UI видел «данные от QUIK приходят» даже когда зоны не меняются.
CREATE TABLE IF NOT EXISTS npr.heartbeat (
id int PRIMARY KEY DEFAULT 1,
last_tick timestamptz NOT NULL DEFAULT NOW(),
tick_count bigint NOT NULL DEFAULT 0
);
INSERT INTO npr.heartbeat (id) VALUES (1) ON CONFLICT DO NOTHING;
-- Кэш «последнее записанное состояние» — нужен триггеру, чтобы понять,
-- изменилась ли зона относительно ранее сохранённой строки. Одна запись на клиентский PK.
CREATE TABLE IF NOT EXISTS npr.last_seen (
firm_id varchar(12) NOT NULL,
client_code varchar(12) NOT NULL,
limit_kind varchar(5) NOT NULL,
curr_code varchar(12) NOT NULL,
tag varchar(4) NOT NULL,
last_ts timestamptz NOT NULL,
last_zone text NOT NULL,
last_uds numeric(19,2),
PRIMARY KEY (firm_id, client_code, limit_kind, curr_code, tag)
);
-- Зона УДС: CALL / WARN / OK / INF / UNKNOWN. Используется и триггером, и отчётами.
CREATE OR REPLACE FUNCTION npr._zone(fl numeric) RETURNS text AS $$
SELECT CASE
WHEN fl IS NULL THEN 'UNKNOWN'
WHEN fl >= 9.99 THEN 'INF'
WHEN fl <= -9.99 THEN 'CALL'
WHEN fl < 0 THEN 'CALL'
WHEN fl < 1 THEN 'CALL'
WHEN fl < 1.5 THEN 'WARN'
ELSE 'OK'
END;
$$ LANGUAGE sql IMMUTABLE;
-- (д) Триггер-функция: новая запись в history только при смене зоны или раз в 30 мин.
-- Фильтр МД / МД+ / С применяется сразу, чтобы не тратить ресурсы на остальных.
CREATE OR REPLACE FUNCTION npr.trg_portfolio_to_history()
RETURNS trigger
LANGUAGE plpgsql
AS $BODY$
DECLARE
new_zone text;
last_ts timestamptz;
last_zone text;
should_write boolean := false;
BEGIN
-- Пульс: обновляем не чаще раза в 10 сек, чтобы не делать тысяч UPDATE на один тик экспортёра.
UPDATE npr.heartbeat
SET last_tick = NOW(), tick_count = tick_count + 1
WHERE id = 1 AND last_tick < NOW() - INTERVAL '10 seconds';
IF NEW."MarginType" NOT IN ('МД','МД+','С') THEN
RETURN NEW;
END IF;
new_zone := npr._zone(NEW."FundsLevel");
SELECT ls.last_ts, ls.last_zone
INTO last_ts, last_zone
FROM npr.last_seen ls
WHERE ls.firm_id = NEW."FirmId"
AND ls.client_code = NEW."ClientCode"
AND ls.limit_kind = NEW."LimitKind"
AND ls.curr_code = NEW."CurrCode"
AND ls.tag = NEW."Tag";
IF last_ts IS NULL THEN
should_write := true; -- впервые видим клиента
ELSIF last_zone IS DISTINCT FROM new_zone THEN
should_write := true; -- переход зоны
ELSIF NOW() - last_ts > INTERVAL '30 minutes' THEN
should_write := true; -- heartbeat
END IF;
IF should_write THEN
INSERT INTO npr.history (
firm_id, client_code, limit_kind, curr_code, margin_type,
portfolio_value, init_margin, min_margin, corrected_margin,
rcv1, rcv2, funds_level, status, risk_level
) VALUES (
NEW."FirmId", NEW."ClientCode", NEW."LimitKind", NEW."CurrCode", NEW."MarginType",
NEW."PortfolioValue", NEW."InitMargin", NEW."MinMargin", NEW."CorrectedMargin",
NEW."RCV1", NEW."RCV2", NEW."FundsLevel", NEW."Status", NEW."RiskLevel"
);
INSERT INTO npr.last_seen (firm_id, client_code, limit_kind, curr_code, tag,
last_ts, last_zone, last_uds)
VALUES (NEW."FirmId", NEW."ClientCode", NEW."LimitKind", NEW."CurrCode", NEW."Tag",
NOW(), new_zone, NEW."FundsLevel")
ON CONFLICT (firm_id, client_code, limit_kind, curr_code, tag)
DO UPDATE SET last_ts = EXCLUDED.last_ts,
last_zone = EXCLUDED.last_zone,
last_uds = EXCLUDED.last_uds;
END IF;
RETURN NEW;
END;
$BODY$;
-- (е) Триггер на "Portfolio" — AFTER INSERT OR UPDATE.
DROP TRIGGER IF EXISTS portfolio_to_npr_history ON "Portfolio";
CREATE TRIGGER portfolio_to_npr_history
AFTER INSERT OR UPDATE ON "Portfolio"
FOR EACH ROW
EXECUTE FUNCTION npr.trg_portfolio_to_history();
-- (е2) Повторный INSERT в "Portfolio" → UPDATE без ошибки.
-- Экспортёр каждый цикл сначала делает подготовленный INSERT по всем строкам
-- и по дубликату ключа переходит к UPDATE. В PostgreSQL 11 каждая ошибка
-- подготовленного INSERT оставляет ~20 КБ в памяти сессии: ~10 тыс. строк/мин
-- дают ≈200 МБ/мин, backend дорастает до OOM и роняет весь кластер (14.09.2026).
-- AFTER UPDATE триггер истории выше срабатывает как обычно.
CREATE OR REPLACE FUNCTION npr.trg_portfolio_insert_as_update()
RETURNS trigger
LANGUAGE plpgsql
AS $BODY$
BEGIN
UPDATE "Portfolio"
SET "MarginType" = NEW."MarginType", "Status" = NEW."Status",
"PortfolioValue" = NEW."PortfolioValue", "InitMargin" = NEW."InitMargin",
"MinMargin" = NEW."MinMargin", "CorrectedMargin" = NEW."CorrectedMargin",
"RCV1" = NEW."RCV1", "RCV2" = NEW."RCV2",
"RiskLevel" = NEW."RiskLevel", "FundsLevel" = NEW."FundsLevel"
WHERE "FirmId" = NEW."FirmId" AND "ClientCode" = NEW."ClientCode"
AND "LimitKind" = NEW."LimitKind" AND "CurrCode" = NEW."CurrCode" AND "Tag" = NEW."Tag";
IF FOUND THEN
RETURN NULL;
END IF;
RETURN NEW;
END;
$BODY$;
DROP TRIGGER IF EXISTS portfolio_insert_as_update ON "Portfolio";
CREATE TRIGGER portfolio_insert_as_update
BEFORE INSERT ON "Portfolio"
FOR EACH ROW
EXECUTE FUNCTION npr.trg_portfolio_insert_as_update();
-- (ж) Прикладная вьюха: УДС с обработкой QUIK-сентинелов и маржинальное требование.
-- QUIK кладёт УДС в диапазон [-9.99, 9.99]: 9.99 = «бесконечность» (капнут сверху),
-- -9.99 = маржин-колл / принудзакрытие (капнут снизу).
CREATE OR REPLACE VIEW npr.v_npr AS
SELECT
ts,
firm_id,
client_code,
margin_type,
portfolio_value,
init_margin,
min_margin,
rcv1,
rcv2,
CASE WHEN funds_level >= 9.99 THEN NULL ELSE funds_level END AS uds,
CASE WHEN funds_level >= 9.99 THEN 'INF'
WHEN funds_level <= -9.99 THEN 'CALL'
ELSE NULL END AS uds_flag,
GREATEST(-rcv1, 0) AS margin_requirement
FROM npr.history;
COMMIT;
-- ===========================================================================
-- Retention-политика: cron-файл /etc/cron.d/npr-retention чистит записи > 2 лет
-- раз в месяц:
-- 0 3 1 * * postgres psql -d quik_export -c \
-- "DELETE FROM npr.history WHERE ts < NOW() - INTERVAL '2 years';"
-- Если нужно партиционирование по месяцам — отдельная миграция.
-- ===========================================================================
-- npr.v_current (текущее состояние по клиентам; снято с рабочей базы 14.09.2026)
CREATE OR REPLACE VIEW npr.v_current AS
SELECT p."FirmId" AS firm_id,
p."ClientCode" AS client_code,
p."LimitKind" AS limit_kind,
p."CurrCode" AS curr_code,
p."Tag" AS tag,
p."MarginType" AS margin_type,
p."RCV1" AS rcv1,
p."RCV2" AS rcv2,
p."FundsLevel" AS funds_level,
p."PortfolioValue" AS portfolio_value,
p."InitMargin" AS init_margin,
p."MinMargin" AS min_margin,
p."CorrectedMargin" AS corrected_margin,
p."Status" AS status,
p."RiskLevel" AS risk_level,
CASE
WHEN p."FundsLevel" >= 9.99 THEN NULL::numeric
ELSE p."FundsLevel"
END AS uds,
CASE
WHEN p."FundsLevel" >= 9.99 THEN 'INF'::text
WHEN p."FundsLevel" <= '-9.99'::numeric THEN 'CALL'::text
ELSE NULL::text
END AS uds_flag,
GREATEST(- p."RCV1", 0::double precision) AS margin_requirement,
npr._zone(p."FundsLevel") AS zone,
hb.last_tick AS ts
FROM "Portfolio" p
CROSS JOIN ( SELECT heartbeat.last_tick
FROM npr.heartbeat
WHERE heartbeat.id = 1) hb
WHERE p."MarginType"::text = ANY (ARRAY['МД'::text, 'МД+'::text, 'С'::text, 'C'::text]);
GRANT SELECT ON npr.v_current TO quik_ro;