Кабинет: оценка портфеля по ценам QUIK и карточка бумаги из справочника депозитария
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
package lkgateway
|
||||
|
||||
// prices.go — рыночные цены бумаг из базы экспорта QUIK (таблицы Securities и Params,
|
||||
// пакет данных «Цены для оценки портфеля»). Нужны кабинету, чтобы показать стоимость
|
||||
// позиции и портфеля. Подключение берём из настроек «НПР / УДС» — это та же база
|
||||
// экспорта; без неё кабинет просто не показывает оценку.
|
||||
//
|
||||
// Цена облигации приходит в процентах от номинала, акции и пая — в рублях за штуку.
|
||||
// Оценка: облигация = количество × номинал × цена / 100 + НКД × количество.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SecurityPrice — цена одной бумаги на момент среза.
|
||||
type SecurityPrice struct {
|
||||
ISIN string `json:"isin"`
|
||||
Board string `json:"board"` // класс QUIK, откуда взята цена
|
||||
Name string `json:"name,omitempty"` // краткое наименование в торговой системе
|
||||
Price float64 `json:"price"` // последняя цена (для облигаций — процент номинала)
|
||||
Prev float64 `json:"prev,omitempty"` // цена предыдущего закрытия
|
||||
Face float64 `json:"face,omitempty"` // номинал
|
||||
Accrued float64 `json:"accrued,omitempty"` // НКД на бумагу
|
||||
InPct bool `json:"in_pct,omitempty"` // цена в процентах от номинала
|
||||
ChangePc float64 `json:"change_pc,omitempty"` // изменение к закрытию, %
|
||||
}
|
||||
|
||||
// Value — стоимость количества бумаг по этой цене.
|
||||
func (p SecurityPrice) Value(qty float64) float64 {
|
||||
if p.Price <= 0 || qty == 0 {
|
||||
return 0
|
||||
}
|
||||
if p.InPct && p.Face > 0 {
|
||||
return qty*p.Face*p.Price/100 + qty*p.Accrued
|
||||
}
|
||||
return qty * p.Price
|
||||
}
|
||||
|
||||
// pricesCache — цены живут минуту: страницу кабинета открывают часто, а цикл экспорта — 30 с.
|
||||
var (
|
||||
priceMu sync.Mutex
|
||||
priceAt time.Time
|
||||
priceLast map[string]SecurityPrice
|
||||
)
|
||||
|
||||
// lkPrices — цены по списку ISIN. Пустая карта, если база экспорта не настроена или
|
||||
// пакет «Цены» не включён: кабинет в этом случае показывает реестр без оценки.
|
||||
func lkPrices(ctx context.Context, rc *RuntimeConfig, isins []string) map[string]SecurityPrice {
|
||||
out := map[string]SecurityPrice{}
|
||||
if rc == nil || len(isins) == 0 {
|
||||
return out
|
||||
}
|
||||
priceMu.Lock()
|
||||
if time.Since(priceAt) < time.Minute && priceLast != nil {
|
||||
for _, isin := range isins {
|
||||
if p, ok := priceLast[strings.ToUpper(isin)]; ok {
|
||||
out[p.ISIN] = p
|
||||
}
|
||||
}
|
||||
priceMu.Unlock()
|
||||
return out
|
||||
}
|
||||
priceMu.Unlock()
|
||||
|
||||
p, err := nprDB(rc)
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
cctx, cancel := context.WithTimeout(ctx, 8*time.Second)
|
||||
defer cancel()
|
||||
// Одна цена на бумагу: берём класс основных торгов (акции TQBR, ОФЗ TQOB,
|
||||
// корпоративные облигации TQCB, фонды TQTF/TQIF), иначе — класс с торгами.
|
||||
rows, err := p.Query(cctx, `
|
||||
SELECT DISTINCT ON (s."ISINCode")
|
||||
upper(s."ISINCode"), s."ClassCode", COALESCE(s."SecShortName", s."SecName", ''),
|
||||
COALESCE(p.last, 0)::float8, COALESCE(p.prevprice, 0)::float8,
|
||||
COALESCE(s."FaceValue", 0)::float8, COALESCE(p.accruedint, 0)::float8
|
||||
FROM "Securities" s JOIN "Params" p ON p."SecCode" = s."SecCode" AND p."ClassCode" = s."ClassCode"
|
||||
WHERE s."ISINCode" <> '' AND (p.last > 0 OR p.prevprice > 0)
|
||||
ORDER BY s."ISINCode",
|
||||
CASE s."ClassCode" WHEN 'TQBR' THEN 1 WHEN 'TQOB' THEN 2 WHEN 'TQCB' THEN 3
|
||||
WHEN 'TQTF' THEN 4 WHEN 'TQIF' THEN 5 WHEN 'TQRD' THEN 6 ELSE 9 END,
|
||||
p.last DESC`)
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
all := map[string]SecurityPrice{}
|
||||
for rows.Next() {
|
||||
var sp SecurityPrice
|
||||
if rows.Scan(&sp.ISIN, &sp.Board, &sp.Name, &sp.Price, &sp.Prev, &sp.Face, &sp.Accrued) != nil {
|
||||
continue
|
||||
}
|
||||
if sp.Price <= 0 {
|
||||
sp.Price = sp.Prev
|
||||
}
|
||||
sp.InPct = sp.Face > 0 && (sp.Board == "TQOB" || sp.Board == "TQCB" || strings.HasSuffix(sp.Board, "_BND"))
|
||||
if sp.Prev > 0 && sp.Price > 0 {
|
||||
sp.ChangePc = (sp.Price - sp.Prev) / sp.Prev * 100
|
||||
}
|
||||
all[sp.ISIN] = sp
|
||||
}
|
||||
rows.Close()
|
||||
if len(all) == 0 {
|
||||
return out
|
||||
}
|
||||
priceMu.Lock()
|
||||
priceLast, priceAt = all, time.Now()
|
||||
priceMu.Unlock()
|
||||
for _, isin := range isins {
|
||||
if sp, ok := all[strings.ToUpper(isin)]; ok {
|
||||
out[sp.ISIN] = sp
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// lkPriceReset — сбросить кэш цен (после смены настроек подключения).
|
||||
func lkPriceReset() {
|
||||
priceMu.Lock()
|
||||
priceLast, priceAt = nil, time.Time{}
|
||||
priceMu.Unlock()
|
||||
}
|
||||
|
||||
// lkApplyPrices — проставляет позициям цену и стоимость, возвращает оценку портфеля
|
||||
// и число бумаг, по которым цена нашлась.
|
||||
func lkApplyPrices(ctx context.Context, rc *RuntimeConfig, pos []ClientPosition) (total float64, priced int) {
|
||||
if len(pos) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
isins := make([]string, 0, len(pos))
|
||||
for _, p := range pos {
|
||||
if p.ISIN != "" {
|
||||
isins = append(isins, p.ISIN)
|
||||
}
|
||||
}
|
||||
px := lkPrices(ctx, rc, isins)
|
||||
for i := range pos {
|
||||
sp, ok := px[strings.ToUpper(pos[i].ISIN)]
|
||||
if !ok || sp.Price <= 0 {
|
||||
continue
|
||||
}
|
||||
pos[i].Price, pos[i].PriceAt, pos[i].ChangePct = sp.Price, sp.Board, sp.ChangePc
|
||||
pos[i].Value = sp.Value(pos[i].Quantity)
|
||||
total += pos[i].Value
|
||||
priced++
|
||||
}
|
||||
return total, priced
|
||||
}
|
||||
Reference in New Issue
Block a user