Files
Bridge-and-Join-s/internal/lkgateway/security_card.go
T

181 lines
7.1 KiB
Go

package lkgateway
// security_card.go — карточка ценной бумаги для кабинета клиента: реквизиты выпуска из
// справочника депозитария (OD_VALUES + действующая версия OD_SHARES), график купонов и
// оферт (OD_COUPONS, OD_OFFERS), цена из базы экспорта QUIK и позиция клиента.
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
)
// SecurityPayment — событие по бумаге: купон, оферта, погашение.
type SecurityPayment struct {
Date string `json:"date"` // ДД.ММ.ГГГГ
Kind string `json:"kind"` // купон | оферта | погашение
Amount float64 `json:"amount,omitempty"` // на одну бумагу
Percent float64 `json:"percent,omitempty"` // ставка купона, % годовых
FixDate string `json:"fix_date,omitempty"`
Paid bool `json:"paid,omitempty"`
}
// SecurityCard — карточка бумаги.
type SecurityCard struct {
ISIN string `json:"isin"`
Name string `json:"name"`
Kind string `json:"kind,omitempty"` // акция | облигация | пай | бумага
Issuer string `json:"issuer,omitempty"` // эмитент
RegNumber string `json:"reg_number,omitempty"`
CFI string `json:"cfi,omitempty"`
Face float64 `json:"face,omitempty"` // номинал
Maturity string `json:"maturity,omitempty"` // дата погашения
Accepted bool `json:"accepted"` // принята на обслуживание
QualOnly bool `json:"qual_only,omitempty"` // только для квалифицированных инвесторов
QualNote string `json:"qual_note,omitempty"`
Price float64 `json:"price,omitempty"`
PriceIn string `json:"price_in,omitempty"` // «₽ за штуку» | «% от номинала»
Board string `json:"board,omitempty"` // класс торгов
ChangePct float64 `json:"change_pct,omitempty"` // изменение к закрытию
Position *ClientPosition `json:"position,omitempty"`
Payments []SecurityPayment `json:"payments,omitempty"`
Note string `json:"note,omitempty"` // почему чего-то нет
}
// securityCard — собирает карточку. Позиция клиента передаётся снаружи (её уже знает
// обработчик портфеля), справочные данные читаются из учётной системы.
func securityCard(ctx context.Context, rc *RuntimeConfig, isin string, pos *ClientPosition) (SecurityCard, error) {
isin = strings.ToUpper(strings.TrimSpace(isin))
card := SecurityCard{ISIN: isin, Position: pos}
if pos != nil {
card.Name = pos.SecurityName
}
if isin == "" {
return card, fmt.Errorf("не указана бумага")
}
if px := lkPrices(ctx, rc, []string{isin}); len(px) > 0 {
p := px[isin]
card.Price, card.Board, card.ChangePct = p.Price, p.Board, p.ChangePc
card.PriceIn = "₽ за штуку"
if p.InPct {
card.PriceIn = "% от номинала"
}
if card.Name == "" {
card.Name = p.Name
}
}
fs := rc.Snapshot().Fancy
if strings.TrimSpace(fs.Server) == "" {
card.Note = "справочник депозитария недоступен"
return card, nil
}
db, dctx, cancel, errS := depoOpen(fs)
if errS != "" {
card.Note = errS
return card, nil
}
defer cancel()
defer db.Close()
_ = dctx
var valID int64
var name, cfi string
var vType, isIn sql.NullInt64
err := db.QueryRowContext(dctx, `SELECT TOP 1 ID, ISNULL(NAME,''), ISNULL(CFI,''), V_TYPE, IS_IN FROM OD_VALUES WHERE ISIN=@p1 ORDER BY ID`,
sql.Named("p1", isin)).Scan(&valID, &name, &cfi, &vType, &isIn)
if err != nil {
card.Note = "бумаги нет в справочнике депозитария"
return card, nil
}
if name != "" {
card.Name = name
}
card.CFI, card.Accepted = cfi, isIn.Valid && isIn.Int64 == 1
card.Kind = securityKind(cfi)
var issuer sql.NullInt64
var nominal sql.NullFloat64
var regNum sql.NullString
var death sql.NullTime
err = db.QueryRowContext(dctx, `SELECT TOP 1 ISSUER, CAST(NOMINAL AS float), NUM_REG, DEATHDATE FROM OD_SHARES
WHERE ID=@p1 AND E_DATE > GETDATE() ORDER BY B_DATE DESC`, sql.Named("p1", valID)).Scan(&issuer, &nominal, &regNum, &death)
if err == nil {
card.Face = nominal.Float64
card.RegNumber = strings.TrimSpace(regNum.String)
if death.Valid && death.Time.Year() < 9000 {
card.Maturity = death.Time.Format("02.01.2006")
}
if issuer.Valid && issuer.Int64 > 0 {
var iname string
if db.QueryRowContext(dctx, `SELECT TOP 1 ISNULL(NAME,'') FROM OD_FACES WHERE ID=@p1 OR SELF_ID=@p1 ORDER BY ISNULL(LAST_FLAG,0) DESC, ID DESC`,
sql.Named("p1", issuer.Int64)).Scan(&iname) == nil {
card.Issuer = strings.TrimSpace(iname)
}
}
}
card.QualOnly, card.QualNote = depoSecurityQualOnly(dctx, db, valID)
// График: купоны за полгода назад и вперёд, ближайшие оферты, погашение.
// Дата выплаты купона: P_DATE, а пока купон не выплачен — конец купонного периода.
rows, err := db.QueryContext(dctx, `SELECT TOP 12 ISNULL(P_DATE, E_DATE) AS PAY_DATE, CAST(ISNULL(SUMMA,0) AS float),
CAST(ISNULL(PERCENT_,0) AS float), FIX_DATE, ISNULL(IS_PAY,0)
FROM OD_COUPONS WHERE SHARE=@p1 AND ISNULL(P_DATE, E_DATE) >= DATEADD(month,-3,GETDATE())
ORDER BY PAY_DATE`, sql.Named("p1", valID))
if err == nil {
for rows.Next() {
var pd, fx sql.NullTime
var summa, pct float64
var isPay sql.NullInt64
if rows.Scan(&pd, &summa, &pct, &fx, &isPay) != nil || !pd.Valid {
continue
}
p := SecurityPayment{Date: pd.Time.Format("02.01.2006"), Kind: "купон", Amount: summa, Percent: pct,
Paid: pd.Time.Before(time.Now())}
if fx.Valid && fx.Time.Year() < 9000 {
p.FixDate = fx.Time.Format("02.01.2006")
}
card.Payments = append(card.Payments, p)
}
rows.Close()
}
orows, err := db.QueryContext(dctx, `SELECT TOP 3 P_DATE, CAST(ISNULL(O_PRICE,0) AS float) FROM OD_OFFERS
WHERE SHARE=@p1 AND CANCEL_DATE IS NULL AND P_DATE >= GETDATE() ORDER BY P_DATE`, sql.Named("p1", valID))
if err == nil {
for orows.Next() {
var pd sql.NullTime
var price float64
if orows.Scan(&pd, &price) == nil && pd.Valid {
card.Payments = append(card.Payments, SecurityPayment{Date: pd.Time.Format("02.01.2006"), Kind: "оферта", Amount: price})
}
}
orows.Close()
}
if card.Maturity != "" {
card.Payments = append(card.Payments, SecurityPayment{Date: card.Maturity, Kind: "погашение", Amount: card.Face})
}
return card, nil
}
// securityKind — вид бумаги по коду CFI (первая буква классификации).
func securityKind(cfi string) string {
if cfi == "" {
return ""
}
switch strings.ToUpper(cfi[:1]) {
case "E":
return "акция"
case "D":
return "облигация"
case "C":
return "пай фонда"
case "R":
return "право"
}
return "ценная бумага"
}