package lkgateway // lkupcoming.go — ближайшие выплаты по бумагам портфеля: купоны, оферты, погашения, // дивиденды. Суммы на бумагу — из справочника депозитария (OD_COUPONS, OD_OFFERS, OD_SHARES), // умножаются на количество в портфеле клиента. Дивиденды по корпоративным действиям (U_CA_ACTS, // DVCA) даются без суммы: размер известен только после решения эмитента. import ( "context" "database/sql" "fmt" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql" "sort" "strings" "time" ) // LKUpcoming — одна ожидаемая выплата. type LKUpcoming struct { Date string `json:"date"` ISIN string `json:"isin"` Security string `json:"security"` Kind string `json:"kind"` // купон | оферта | погашение | дивиденды PerUnit float64 `json:"per_unit,omitempty"` // на одну бумагу, ₽ Quantity float64 `json:"quantity"` Total float64 `json:"total,omitempty"` // на весь пакет клиента, ₽ Percent float64 `json:"percent,omitempty"` sortKey time.Time } // lkUpcoming — выплаты по позициям в ближайшие days дней. func lkUpcoming(ctx context.Context, s FancyMSSQLSettings, positions []LKPosition, days int) []LKUpcoming { if strings.TrimSpace(s.Server) == "" || len(positions) == 0 { return nil } qty := map[string]float64{} names := map[string]string{} var isins []string for _, p := range positions { isin := strings.ToUpper(strings.TrimSpace(p.ISIN)) if isin == "" || p.Quantity <= 0 { continue } if _, ok := qty[isin]; !ok { isins = append(isins, isin) } qty[isin] += p.Quantity names[isin] = p.Name } if len(isins) == 0 { return nil } conn, err := fancyOpen(s, s.DepoDB()) if err != nil { return nil } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 20*time.Second) defer cancel() from := time.Now().Truncate(24 * time.Hour) to := from.AddDate(0, 0, days) params := make([]string, 0, len(isins)) args := []any{sql.Named("f", from), sql.Named("t", to)} for i, isin := range isins { params = append(params, fmt.Sprintf("@i%d", i)) args = append(args, sql.Named(fmt.Sprintf("i%d", i), isin)) } in := strings.Join(params, ",") var out []LKUpcoming add := func(isin string, d time.Time, kind string, per, pct float64) { u := LKUpcoming{Date: d.Format("02.01.2006"), ISIN: isin, Security: names[isin], Kind: kind, PerUnit: per, Quantity: qty[isin], Percent: pct, sortKey: d} if per > 0 { u.Total = per * qty[isin] } out = append(out, u) } // Купоны: дата выплаты — P_DATE, до выплаты — конец периода. IS_PAY здесь означает // «купон выплачиваемый» (не «уже выплачен»), поэтому по нему не фильтруем. rows, err := conn.QueryContext(cctx, fmt.Sprintf(fsql.Up01, in), args...) if err == nil { for rows.Next() { var isin string var d sql.NullTime var summa, pct float64 if rows.Scan(&isin, &d, &summa, &pct) == nil && d.Valid { add(isin, d.Time, "купон", summa, pct) } } rows.Close() } // Оферты. rows, err = conn.QueryContext(cctx, fmt.Sprintf(fsql.Up02, in), args...) if err == nil { for rows.Next() { var isin string var d sql.NullTime var price float64 if rows.Scan(&isin, &d, &price) == nil && d.Valid { add(isin, d.Time, "оферта", price, 0) } } rows.Close() } // Погашения: по номиналу в дату DEATHDATE. rows, err = conn.QueryContext(cctx, fmt.Sprintf(fsql.Up03, in), args...) if err == nil { for rows.Next() { var isin string var d sql.NullTime var nominal float64 if rows.Scan(&isin, &d, &nominal) == nil && d.Valid { add(isin, d.Time, "погашение", nominal, 0) } } rows.Close() } // Дивиденды (корпоративные действия DVCA): дата фиксации или выплаты, суммы нет. rows, err = conn.QueryContext(cctx, fmt.Sprintf(fsql.Up04, in), args...) if err == nil { for rows.Next() { var isin string var d sql.NullTime if rows.Scan(&isin, &d) == nil && d.Valid && d.Time.Year() < 9000 { add(isin, d.Time, "дивиденды", 0, 0) } } rows.Close() } sort.Slice(out, func(i, j int) bool { return out[i].sortKey.Before(out[j].sortKey) }) return out }