package lkgateway // prices.go — рыночные цены бумаг из базы экспорта QUIK (таблицы Securities и Params, // пакет данных «Цены для оценки портфеля»). Нужны кабинету, чтобы показать стоимость // позиции и портфеля. Подключение берём из настроек «НПР / УДС» — это та же база // экспорта; без неё кабинет просто не показывает оценку. // // Цена облигации приходит в процентах от номинала, акции и пая — в рублях за штуку. // Оценка: облигация = количество × номинал × цена / 100 + НКД × количество. import ( "context" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql" "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"` // изменение к закрытию, % // FaceCur — валюта номинала (USD, EUR, CNY…); пусто или SUR — рубли. У «замещающих» // и еврооблигаций номинал валютный, поэтому тело переводится в рубли по курсу FX. FaceCur string `json:"face_cur,omitempty"` FX float64 `json:"fx,omitempty"` // курс валюты номинала к рублю (1 для рублей) FXAt time.Time `json:"fx_at,omitempty"` // дата курса } // Value — стоимость количества бумаг по этой цене. func (p SecurityPrice) Value(qty float64) float64 { if p.Price <= 0 || qty == 0 { return 0 } if p.InPct && p.Face > 0 { fx := p.FX if fx <= 0 { if p.FaceCur == "" || strings.EqualFold(p.FaceCur, "SUR") || strings.EqualFold(p.FaceCur, "RUB") { fx = 1 } else { return 0 // валютный номинал без курса: оценку не выдумываем } } // НКД экспорт отдаёт в рублях, тело — в процентах от номинала в валюте выпуска. return qty*p.Face*p.Price/100*fx + 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() fxRates := lkFxRates(ctx, rc) // для выпусков с валютным номиналом // Одна цена на бумагу: берём класс основных торгов (акции TQBR, ОФЗ TQOB, // корпоративные облигации TQCB, фонды TQTF/TQIF), иначе — класс с торгами. rows, err := p.Query(cctx, fsql.Pr01) 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, &sp.FaceCur) != 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 } if fx := lkFxFor(fxRates, sp.FaceCur); fx.Rate > 0 { sp.FX, sp.FXAt = fx.Rate, fx.At } 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 }