package lkgateway // lkonline.go — портфель клиента «сейчас» по экспорту биржевой информации из QUIK. // // Это то, что клиент видит у брокера: позиции с количеством, средней ценой покупки, текущей // ценой и результатом, деньги по валютам, стоимость портфеля. Источник — таблицы экспорта // DepoLimits (позиции), MoneyLimits (деньги), Securities/Params (бумаги и цены). Клиент // определяется кодом QUIK, полученным из номера договора (lkQuikCodes). import ( "context" "fmt" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql" "sort" "strconv" "strings" "time" ) // LKPortfolio — портфель для кабинета (онлайн или на дату — одна форма). type LKPortfolio struct { Source string `json:"source"` // quik | spectre AsOf string `json:"as_of"` // на какой момент Total float64 `json:"total"` // стоимость портфеля, ₽ (бумаги + деньги в рублях) Securities float64 `json:"securities_value"` // стоимость бумаг, ₽ CashRUB float64 `json:"cash_rub"` // деньги в рублях Cost float64 `json:"cost"` // вложено (по средней цене / балансовой стоимости), ₽ PnL float64 `json:"pnl"` // результат по позициям, ₽ PnLPct float64 `json:"pnl_pct"` // результат, % Priced int `json:"priced"` // позиций с ценой Cash []LKCash `json:"cash"` Positions []LKPosition `json:"positions"` Note string `json:"note,omitempty"` } // LKCash — деньги в одной валюте. type LKCash struct { Currency string `json:"currency"` // SUR, USD, CNY … Amount float64 `json:"amount"` } // LKPosition — позиция по бумаге. type LKPosition struct { ISIN string `json:"isin"` Code string `json:"code,omitempty"` // код в торговой системе Name string `json:"name"` Kind string `json:"kind"` // акция | облигация | фонд | … Quantity float64 `json:"quantity"` AvgPrice float64 `json:"avg_price,omitempty"` // средняя цена покупки (за бумагу, в тех же единицах, что и Price) Price float64 `json:"price,omitempty"` // текущая цена InPct bool `json:"in_pct,omitempty"` // цены в процентах номинала (облигации) Value float64 `json:"value"` // стоимость, ₽ Cost float64 `json:"cost,omitempty"` // вложено, ₽ PnL float64 `json:"pnl"` // результат, ₽ PnLPct float64 `json:"pnl_pct"` // результат, % DayPct float64 `json:"day_pct,omitempty"` // изменение за день, % Share float64 `json:"share"` // доля в портфеле, % Currency string `json:"currency,omitempty"` Note string `json:"note,omitempty"` } // lkKindByClass — вид бумаги по классу QUIK. func lkKindByClass(class string) string { switch { case class == "TQBR", class == "TQPI", strings.HasPrefix(class, "SPBXM"): return "акция" case class == "TQOB", class == "TQCB", class == "TQIR", class == "TQRD", strings.HasSuffix(class, "_BND"): return "облигация" case class == "TQTF", class == "TQIF", class == "TQTD": return "фонд" case class == "CETS": return "валюта" } return "бумага" } // lkOnlinePortfolio — портфель по экспорту QUIK. Ошибка — только если базы экспорта нет. func lkOnlinePortfolio(ctx context.Context, rc *RuntimeConfig, contract string) (*LKPortfolio, error) { codes := lkQuikCodes(contract) if len(codes) == 0 { return nil, fmt.Errorf("не определён номер договора") } p, err := nprDB(rc) if err != nil { return nil, fmt.Errorf("база экспорта QUIK недоступна: %v", err) } cctx, cancel := context.WithTimeout(ctx, 12*time.Second) defer cancel() out := &LKPortfolio{Source: "quik"} // Позиции: свежая строка по каждому (клиент, бумага, торговый счёт), затем сумма по бумаге. rows, err := p.Query(cctx, fsql.On01, codes) if err != nil { return nil, fmt.Errorf("позиции: %v", err) } type raw struct { code string qty, avg float64 } var pos []raw var asOf time.Time for rows.Next() { var r raw var ld *time.Time if rows.Scan(&r.code, &r.qty, &r.avg, &ld) == nil { pos = append(pos, r) if ld != nil && ld.After(asOf) { asOf = *ld } } } rows.Close() // Бумаги: ISIN, имя, класс (основные режимы торгов в приоритете), номинал. secByCode := map[string]struct { isin, name, class string face float64 }{} if len(pos) > 0 { codesList := make([]string, 0, len(pos)) for _, r := range pos { codesList = append(codesList, r.code) } srows, err := p.Query(cctx, fsql.On02, codesList) if err == nil { for srows.Next() { var code, isin, name, class string var face float64 if srows.Scan(&code, &isin, &name, &class, &face) == nil { secByCode[code] = struct { isin, name, class string face float64 }{isin, name, class, face} } } srows.Close() } } isins := make([]string, 0, len(pos)) for _, r := range pos { if s, ok := secByCode[r.code]; ok && s.isin != "" { isins = append(isins, s.isin) } } prices := lkPrices(ctx, rc, isins) fxUsed := map[string]SecurityPrice{} for _, r := range pos { s := secByCode[r.code] lp := LKPosition{ISIN: s.isin, Code: r.code, Name: s.name, Kind: lkKindByClass(s.class), Quantity: r.qty, AvgPrice: r.avg, Currency: "SUR"} if lp.Name == "" { lp.Name = r.code } if px, ok := prices[s.isin]; ok && px.Price > 0 { lp.Price, lp.InPct, lp.DayPct = px.Price, px.InPct, px.ChangePc lp.Value = px.Value(r.qty) if lp.Value > 0 { out.Priced++ } switch { case lp.Value <= 0 && px.FaceCur != "": lp.Note = "номинал в " + px.FaceCur + ": нет курса для оценки" case px.FX > 0 && px.FX != 1: lp.Note = "номинал в " + px.FaceCur fxUsed[px.FaceCur] = px // курс покажем одной строкой под таблицей } if r.avg > 0 && lp.Value > 0 { // средняя цена в тех же единицах, что и котировка: для облигаций — процент номинала cost := SecurityPrice{Price: r.avg, Face: px.Face, InPct: px.InPct, FaceCur: px.FaceCur, FX: px.FX} lp.Cost = cost.Value(r.qty) lp.PnL = lp.Value - lp.Cost if lp.Cost != 0 { lp.PnLPct = lp.PnL / lp.Cost * 100 } } } else { lp.Note = "нет биржевой цены" } out.Positions = append(out.Positions, lp) } // Деньги по валютам. crows, err := p.Query(cctx, fsql.On03, codes) if err == nil { for crows.Next() { var c LKCash var ld *time.Time if crows.Scan(&c.Currency, &c.Amount, &ld) == nil { out.Cash = append(out.Cash, c) if c.Currency == "SUR" || c.Currency == "RUB" { out.CashRUB += c.Amount } if ld != nil && ld.After(asOf) { asOf = *ld } } } crows.Close() } if !asOf.IsZero() { out.AsOf = asOf.Format("02.01.2006") } if len(fxUsed) > 0 { var parts []string for cur, px := range fxUsed { p := cur + " " + fmtRate(px.FX) + " ₽" if !px.FXAt.IsZero() { p += " от " + px.FXAt.Format("02.01.2006") } parts = append(parts, p) } sort.Strings(parts) out.Note = "выпуски с валютным номиналом пересчитаны по курсу " + strings.Join(parts, ", ") } lkPortfolioTotals(out) return out, nil } // fmtRate — курс валюты с двумя знаками. func fmtRate(v float64) string { return strconv.FormatFloat(v, 'f', 2, 64) } // lkPortfolioTotals — итоги и доли. func lkPortfolioTotals(p *LKPortfolio) { p.Securities, p.Cost, p.PnL = 0, 0, 0 for _, pos := range p.Positions { p.Securities += pos.Value p.Cost += pos.Cost p.PnL += pos.PnL } p.Total = p.Securities + p.CashRUB if p.Cost != 0 { p.PnLPct = p.PnL / p.Cost * 100 } for i := range p.Positions { if p.Total > 0 { p.Positions[i].Share = p.Positions[i].Value / p.Total * 100 } } sort.SliceStable(p.Positions, func(i, j int) bool { return p.Positions[i].Value > p.Positions[j].Value }) }