Кабинет: верная оценка валютных выпусков и читаемые числа
- prices.go: у выпуска берётся валюта номинала (FaceUnit); «замещающие» и еврооблигации котируются в процентах от валютного номинала, поэтому тело переводится в рубли по курсу, НКД экспорт отдаёт уже в рублях. Без курса оценка не выдумывается - lkfx.go: официальные курсы валют из учётной системы (OD_VALUES_RATES, MARKET=0; DEPO, затем SPECTRE — берём свежее), кэш на час; дата курса показывается под таблицей - БР-719 онлайн: было 17 млн ₽ (валютные считались рублёвыми), стало 522 млн ₽ — сходится с архивом брокерского учёта (НОВАТЭК 001Р-02: 162,65 млн против 162,32 млн на дату среза) - вёрстка: отступ между числовыми колонками (числа больше не слипаются), больше воздуха между показателями в шапке, кнопка «Новое поручение» компактнее, цены от 10 — два знака - структура портфеля: плитки с долей и суммой вместо строки в одну линию; блок денег показывается, только когда валют несколько Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
@@ -0,0 +1,91 @@
|
|||||||
|
package lkgateway
|
||||||
|
|
||||||
|
// lkfx.go — курсы валют к рублю для оценки валютных выпусков.
|
||||||
|
//
|
||||||
|
// Номинал «замещающих» и еврооблигаций в экспорте QUIK задан в валюте (FaceUnit = USD, EUR,
|
||||||
|
// CNY), а цена — в процентах от него. Чтобы показать стоимость в рублях, нужен курс. В базе
|
||||||
|
// экспорта курсов нет (валютный рынок не выгружается), поэтому берём официальный курс из
|
||||||
|
// учётной системы: OD_VALUES_RATES с MARKET = 0 — сначала DEPO, затем SPECTRE, и берём более
|
||||||
|
// свежий. Дата курса показывается клиенту: курс может отставать, и выдавать оценку за
|
||||||
|
// сегодняшнюю нельзя.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LKFxRate — курс валюты к рублю и дата, на которую он установлен.
|
||||||
|
type LKFxRate struct {
|
||||||
|
Rate float64
|
||||||
|
At time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var fxCache = struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
at time.Time
|
||||||
|
m map[string]LKFxRate
|
||||||
|
}{}
|
||||||
|
|
||||||
|
// lkFxRates — курсы валют (ключ — код валюты в верхнем регистре), кэш на час.
|
||||||
|
func lkFxRates(ctx context.Context, s FancyMSSQLSettings) map[string]LKFxRate {
|
||||||
|
fxCache.mu.Lock()
|
||||||
|
if fxCache.m != nil && time.Since(fxCache.at) < time.Hour {
|
||||||
|
m := fxCache.m
|
||||||
|
fxCache.mu.Unlock()
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
fxCache.mu.Unlock()
|
||||||
|
out := map[string]LKFxRate{}
|
||||||
|
if strings.TrimSpace(s.Server) == "" {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
for _, db := range []string{s.DepoDB(), s.SpectreDB()} {
|
||||||
|
conn, err := fancyOpen(s, db)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||||
|
rows, err := conn.QueryContext(cctx, `SELECT UPPER(LTRIM(RTRIM(v.SYSNAME))), r.OFICDATE, CAST(r.RATE AS float)
|
||||||
|
FROM OD_VALUES_RATES r JOIN OD_VALUES v ON v.ID = r.VALUE_ID
|
||||||
|
WHERE v.V_TYPE = 1 AND r.MARKET = 0 AND r.RATE > 0
|
||||||
|
AND r.OFICDATE = (SELECT MAX(x.OFICDATE) FROM OD_VALUES_RATES x WHERE x.VALUE_ID = r.VALUE_ID AND x.MARKET = 0 AND x.RATE > 0)`)
|
||||||
|
if err == nil {
|
||||||
|
for rows.Next() {
|
||||||
|
var code string
|
||||||
|
var at sql.NullTime
|
||||||
|
var rate float64
|
||||||
|
if rows.Scan(&code, &at, &rate) != nil || rate <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cur := LKFxRate{Rate: rate}
|
||||||
|
if at.Valid {
|
||||||
|
cur.At = at.Time
|
||||||
|
}
|
||||||
|
// из двух баз берём курс посвежее
|
||||||
|
if old, ok := out[code]; !ok || cur.At.After(old.At) {
|
||||||
|
out[code] = cur
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows.Close()
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
fxCache.mu.Lock()
|
||||||
|
fxCache.m, fxCache.at = out, time.Now()
|
||||||
|
fxCache.mu.Unlock()
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// lkFxFor — курс валюты к рублю: рубль — единица, неизвестная валюта — 0 (оценивать нельзя).
|
||||||
|
func lkFxFor(rates map[string]LKFxRate, cur string) LKFxRate {
|
||||||
|
c := strings.ToUpper(strings.TrimSpace(cur))
|
||||||
|
switch c {
|
||||||
|
case "", "SUR", "RUB", "RUR":
|
||||||
|
return LKFxRate{Rate: 1}
|
||||||
|
}
|
||||||
|
return rates[c]
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -151,6 +152,7 @@ func lkOnlinePortfolio(ctx context.Context, rc *RuntimeConfig, contract string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
prices := lkPrices(ctx, rc, isins)
|
prices := lkPrices(ctx, rc, isins)
|
||||||
|
fxUsed := map[string]SecurityPrice{}
|
||||||
|
|
||||||
for _, r := range pos {
|
for _, r := range pos {
|
||||||
s := secByCode[r.code]
|
s := secByCode[r.code]
|
||||||
@@ -161,10 +163,19 @@ func lkOnlinePortfolio(ctx context.Context, rc *RuntimeConfig, contract string)
|
|||||||
if px, ok := prices[s.isin]; ok && px.Price > 0 {
|
if px, ok := prices[s.isin]; ok && px.Price > 0 {
|
||||||
lp.Price, lp.InPct, lp.DayPct = px.Price, px.InPct, px.ChangePc
|
lp.Price, lp.InPct, lp.DayPct = px.Price, px.InPct, px.ChangePc
|
||||||
lp.Value = px.Value(r.qty)
|
lp.Value = px.Value(r.qty)
|
||||||
out.Priced++
|
if lp.Value > 0 {
|
||||||
if r.avg > 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}
|
cost := SecurityPrice{Price: r.avg, Face: px.Face, InPct: px.InPct, FaceCur: px.FaceCur, FX: px.FX}
|
||||||
lp.Cost = cost.Value(r.qty)
|
lp.Cost = cost.Value(r.qty)
|
||||||
lp.PnL = lp.Value - lp.Cost
|
lp.PnL = lp.Value - lp.Cost
|
||||||
if lp.Cost != 0 {
|
if lp.Cost != 0 {
|
||||||
@@ -203,10 +214,25 @@ func lkOnlinePortfolio(ctx context.Context, rc *RuntimeConfig, contract string)
|
|||||||
if !asOf.IsZero() {
|
if !asOf.IsZero() {
|
||||||
out.AsOf = asOf.Format("02.01.2006")
|
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)
|
lkPortfolioTotals(out)
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fmtRate — курс валюты с двумя знаками.
|
||||||
|
func fmtRate(v float64) string { return strconv.FormatFloat(v, 'f', 2, 64) }
|
||||||
|
|
||||||
// lkPortfolioTotals — итоги и доли.
|
// lkPortfolioTotals — итоги и доли.
|
||||||
func lkPortfolioTotals(p *LKPortfolio) {
|
func lkPortfolioTotals(p *LKPortfolio) {
|
||||||
p.Securities, p.Cost, p.PnL = 0, 0, 0
|
p.Securities, p.Cost, p.PnL = 0, 0, 0
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ type SecurityPrice struct {
|
|||||||
Accrued float64 `json:"accrued,omitempty"` // НКД на бумагу
|
Accrued float64 `json:"accrued,omitempty"` // НКД на бумагу
|
||||||
InPct bool `json:"in_pct,omitempty"` // цена в процентах от номинала
|
InPct bool `json:"in_pct,omitempty"` // цена в процентах от номинала
|
||||||
ChangePc float64 `json:"change_pc,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 — стоимость количества бумаг по этой цене.
|
// Value — стоимость количества бумаг по этой цене.
|
||||||
@@ -34,7 +39,16 @@ func (p SecurityPrice) Value(qty float64) float64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if p.InPct && p.Face > 0 {
|
if p.InPct && p.Face > 0 {
|
||||||
return qty*p.Face*p.Price/100 + qty*p.Accrued
|
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
|
return qty * p.Price
|
||||||
}
|
}
|
||||||
@@ -71,13 +85,14 @@ func lkPrices(ctx context.Context, rc *RuntimeConfig, isins []string) map[string
|
|||||||
}
|
}
|
||||||
cctx, cancel := context.WithTimeout(ctx, 8*time.Second)
|
cctx, cancel := context.WithTimeout(ctx, 8*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
fxRates := lkFxRates(ctx, rc.Snapshot().Fancy) // для выпусков с валютным номиналом
|
||||||
// Одна цена на бумагу: берём класс основных торгов (акции TQBR, ОФЗ TQOB,
|
// Одна цена на бумагу: берём класс основных торгов (акции TQBR, ОФЗ TQOB,
|
||||||
// корпоративные облигации TQCB, фонды TQTF/TQIF), иначе — класс с торгами.
|
// корпоративные облигации TQCB, фонды TQTF/TQIF), иначе — класс с торгами.
|
||||||
rows, err := p.Query(cctx, `
|
rows, err := p.Query(cctx, `
|
||||||
SELECT DISTINCT ON (s."ISINCode")
|
SELECT DISTINCT ON (s."ISINCode")
|
||||||
upper(s."ISINCode"), s."ClassCode", COALESCE(s."SecShortName", s."SecName", ''),
|
upper(s."ISINCode"), s."ClassCode", COALESCE(s."SecShortName", s."SecName", ''),
|
||||||
COALESCE(p.last, 0)::float8, COALESCE(p.prevprice, 0)::float8,
|
COALESCE(p.last, 0)::float8, COALESCE(p.prevprice, 0)::float8,
|
||||||
COALESCE(s."FaceValue", 0)::float8, COALESCE(p.accruedint, 0)::float8
|
COALESCE(s."FaceValue", 0)::float8, COALESCE(p.accruedint, 0)::float8, COALESCE(s."FaceUnit", '')
|
||||||
FROM "Securities" s JOIN "Params" p ON p."SecCode" = s."SecCode" AND p."ClassCode" = s."ClassCode"
|
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)
|
WHERE s."ISINCode" <> '' AND (p.last > 0 OR p.prevprice > 0)
|
||||||
ORDER BY s."ISINCode",
|
ORDER BY s."ISINCode",
|
||||||
@@ -90,7 +105,7 @@ func lkPrices(ctx context.Context, rc *RuntimeConfig, isins []string) map[string
|
|||||||
all := map[string]SecurityPrice{}
|
all := map[string]SecurityPrice{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var sp SecurityPrice
|
var sp SecurityPrice
|
||||||
if rows.Scan(&sp.ISIN, &sp.Board, &sp.Name, &sp.Price, &sp.Prev, &sp.Face, &sp.Accrued) != nil {
|
if rows.Scan(&sp.ISIN, &sp.Board, &sp.Name, &sp.Price, &sp.Prev, &sp.Face, &sp.Accrued, &sp.FaceCur) != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if sp.Price <= 0 {
|
if sp.Price <= 0 {
|
||||||
@@ -100,6 +115,9 @@ func lkPrices(ctx context.Context, rc *RuntimeConfig, isins []string) map[string
|
|||||||
if sp.Prev > 0 && sp.Price > 0 {
|
if sp.Prev > 0 && sp.Price > 0 {
|
||||||
sp.ChangePc = (sp.Price - sp.Prev) / sp.Prev * 100
|
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
|
all[sp.ISIN] = sp
|
||||||
}
|
}
|
||||||
rows.Close()
|
rows.Close()
|
||||||
|
|||||||
+18
-6
@@ -94,15 +94,21 @@ func New(cfg Config) (*Server, error) {
|
|||||||
}
|
}
|
||||||
return fmtMoney(f)
|
return fmtMoney(f)
|
||||||
},
|
},
|
||||||
"fmtprice": func(f float64) string { // цена: до 4 знаков, без хвостовых нулей, с пробелами разрядов
|
"fmtprice": func(f float64) string { // цена: крупные — 2 знака, мелкие — до 4
|
||||||
if f == 0 {
|
if f == 0 {
|
||||||
return "—"
|
return "—"
|
||||||
}
|
}
|
||||||
s := strconv.FormatFloat(f, 'f', 4, 64)
|
digits := 4
|
||||||
s = strings.TrimRight(strings.TrimRight(s, "0"), ".")
|
if f >= 10 || f <= -10 {
|
||||||
whole, frac := s, ""
|
digits = 2
|
||||||
if i := strings.IndexByte(s, '.'); i >= 0 {
|
}
|
||||||
whole, frac = s[:i], s[i+1:]
|
str := strconv.FormatFloat(f, 'f', digits, 64)
|
||||||
|
if strings.Contains(str, ".") {
|
||||||
|
str = strings.TrimRight(strings.TrimRight(str, "0"), ".")
|
||||||
|
}
|
||||||
|
whole, frac := str, ""
|
||||||
|
if i := strings.IndexByte(str, '.'); i >= 0 {
|
||||||
|
whole, frac = str[:i], str[i+1:]
|
||||||
}
|
}
|
||||||
neg := strings.HasPrefix(whole, "-")
|
neg := strings.HasPrefix(whole, "-")
|
||||||
whole = strings.TrimPrefix(whole, "-")
|
whole = strings.TrimPrefix(whole, "-")
|
||||||
@@ -781,6 +787,12 @@ func (s *Server) buildHandler() http.Handler {
|
|||||||
}
|
}
|
||||||
data["P"], data["Contract"], data["FullName"] = out.Portfolio, out.Contract, out.FullName
|
data["P"], data["Contract"], data["FullName"] = out.Portfolio, out.Contract, out.FullName
|
||||||
data["Structure"], data["Upcoming"] = portfolioStructure(out.Portfolio), out.Upcoming
|
data["Structure"], data["Upcoming"] = portfolioStructure(out.Portfolio), out.Upcoming
|
||||||
|
// Рубли и так видны в шапке и структуре — отдельный блок нужен, когда валют несколько.
|
||||||
|
showCash := len(out.Portfolio.Cash) > 1
|
||||||
|
if len(out.Portfolio.Cash) == 1 && out.Portfolio.Cash[0].Currency != "SUR" {
|
||||||
|
showCash = true
|
||||||
|
}
|
||||||
|
data["ShowCash"] = showCash
|
||||||
var upTotal float64
|
var upTotal float64
|
||||||
for _, u := range out.Upcoming {
|
for _, u := range out.Upcoming {
|
||||||
upTotal += u.Total
|
upTotal += u.Total
|
||||||
|
|||||||
+19
-10
@@ -39,18 +39,22 @@ a{color:var(--acc)}a:hover{color:#1e3395}
|
|||||||
.tabform{display:flex;gap:8px;align-items:center;margin-left:8px}
|
.tabform{display:flex;gap:8px;align-items:center;margin-left:8px}
|
||||||
.tabform input[type=date]{padding:7px 10px;border:1px solid var(--line-2);border-radius:3px;background:#fff;font:inherit;font-size:14px}
|
.tabform input[type=date]{padding:7px 10px;border:1px solid var(--line-2);border-radius:3px;background:#fff;font:inherit;font-size:14px}
|
||||||
.pos{color:var(--ok-tx)}.neg{color:var(--err-tx)}
|
.pos{color:var(--ok-tx)}.neg{color:var(--err-tx)}
|
||||||
.sbar{display:flex;height:14px;border-radius:3px;overflow:hidden;background:var(--line);gap:2px}
|
.sbar{display:flex;height:18px;border-radius:3px;overflow:hidden;background:var(--line);gap:2px}
|
||||||
.sbar span{display:block;height:100%;min-width:2px}
|
.sbar span{display:block;height:100%;min-width:3px}
|
||||||
.slegend{display:flex;gap:18px;flex-wrap:wrap;margin-top:10px;font-size:13.5px}
|
.slegend{display:flex;gap:10px;flex-wrap:wrap;margin-top:12px}
|
||||||
.slegend i{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:7px;vertical-align:-1px}
|
.seg{display:flex;flex-direction:column;gap:3px;padding:9px 14px 10px;background:var(--sheet);border:1px solid var(--line-2);border-left-width:3px;border-radius:3px;min-width:150px}
|
||||||
|
.seg .t{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--ink-2)}
|
||||||
|
.seg .p{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:19px;font-weight:700;line-height:1.1}
|
||||||
|
.seg .v{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:12.5px;color:var(--mut)}
|
||||||
|
.seg i{display:inline-block;width:9px;height:9px;border-radius:2px}
|
||||||
.kind{font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--mut)}
|
.kind{font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--mut)}
|
||||||
.cta{display:inline-flex;align-items:center;gap:8px;min-height:38px;padding:0 14px;background:var(--amber);color:var(--ink);text-decoration:none;border:0;border-radius:3px;cursor:pointer;font:inherit}
|
.cta{display:inline-flex;align-items:center;gap:6px;min-height:30px;padding:0 11px;background:var(--amber);color:var(--ink);text-decoration:none;border:0;border-radius:3px;cursor:pointer;font:inherit;font-size:10px}
|
||||||
.cta:hover{filter:brightness(1.05);color:var(--ink)}
|
.cta:hover{filter:brightness(1.05);color:var(--ink)}
|
||||||
.board{background:var(--board-2);color:#f3f0e7;border-bottom:3px solid var(--amber);padding:22px 20px}
|
.board{background:var(--board-2);color:#f3f0e7;border-bottom:3px solid var(--amber);padding:22px 20px}
|
||||||
.board .big{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:40px;font-weight:700;letter-spacing:-.02em;line-height:1;position:relative;display:inline-block}
|
.board .big{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:40px;font-weight:700;letter-spacing:-.02em;line-height:1;position:relative;display:inline-block}
|
||||||
.board .big::after{content:"";position:absolute;left:0;right:0;top:50%;height:1px;background:rgba(0,0,0,.28)}
|
.board .big::after{content:"";position:absolute;left:0;right:0;top:50%;height:1px;background:rgba(0,0,0,.28)}
|
||||||
.board-row{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:20px;max-width:1180px;margin:0 auto}
|
.board-row{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:20px;max-width:1180px;margin:0 auto}
|
||||||
.stats{display:flex;gap:28px;flex-wrap:wrap}
|
.stats{display:flex;gap:14px 46px;flex-wrap:wrap}
|
||||||
.stat{display:flex;flex-direction:column;gap:3px}
|
.stat{display:flex;flex-direction:column;gap:3px}
|
||||||
.stat .v{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:22px}
|
.stat .v{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:22px}
|
||||||
.imp{background:var(--board);color:#f3f0e7;padding:10px 20px;font-size:13px;display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
.imp{background:var(--board);color:#f3f0e7;padding:10px 20px;font-size:13px;display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
||||||
@@ -62,7 +66,7 @@ h1{font-size:23px;margin:0 0 6px;letter-spacing:-.01em}h2{font-size:15px;margin:
|
|||||||
table{width:100%;border-collapse:collapse;font-size:14px}
|
table{width:100%;border-collapse:collapse;font-size:14px}
|
||||||
th{font-family:var(--mono);font-size:10.5px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--mut);text-align:left;padding:0 12px 9px 0;border-bottom:1px solid var(--line-2);white-space:nowrap}
|
th{font-family:var(--mono);font-size:10.5px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--mut);text-align:left;padding:0 12px 9px 0;border-bottom:1px solid var(--line-2);white-space:nowrap}
|
||||||
td{padding:12px 12px 12px 0;border-bottom:1px solid var(--line);vertical-align:middle}
|
td{padding:12px 12px 12px 0;border-bottom:1px solid var(--line);vertical-align:middle}
|
||||||
td.num,th.num{text-align:right;padding-right:0;font-family:var(--mono);font-variant-numeric:tabular-nums}
|
td.num,th.num{text-align:right;padding-right:0;padding-left:28px;white-space:nowrap;font-family:var(--mono);font-variant-numeric:tabular-nums}
|
||||||
tr.grp td{background:#e4e1d8;border-bottom:1px solid var(--line-2);padding:7px 0 7px 8px}
|
tr.grp td{background:#e4e1d8;border-bottom:1px solid var(--line-2);padding:7px 0 7px 8px}
|
||||||
.sheet{background:var(--sheet);border:1px solid var(--line-2);padding:20px}
|
.sheet{background:var(--sheet);border:1px solid var(--line-2);padding:20px}
|
||||||
.chip{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:3px;font-family:var(--mono);font-size:11px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;background:var(--neu-bg);color:var(--ink-2)}
|
.chip{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:3px;font-family:var(--mono);font-size:11px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;background:var(--neu-bg);color:var(--ink-2)}
|
||||||
@@ -155,7 +159,7 @@ input[type=checkbox]{width:20px;min-height:20px;height:20px}
|
|||||||
<div>
|
<div>
|
||||||
<span class="lbl" style="color:var(--dim)">Стоимость портфеля{{if eq .View "archive"}} на {{$p.AsOf}}{{end}}</span>
|
<span class="lbl" style="color:var(--dim)">Стоимость портфеля{{if eq .View "archive"}} на {{$p.AsOf}}{{end}}</span>
|
||||||
<div class="big" style="margin-top:8px">{{fmtmoney $p.Total}} ₽</div>
|
<div class="big" style="margin-top:8px">{{fmtmoney $p.Total}} ₽</div>
|
||||||
<div class="n" style="font-size:13px;color:var(--dim);margin-top:6px">{{with .FullName}}{{.}} · {{end}}договор {{.Contract}}{{if and (ne .View "archive") $p.AsOf}} · данные на {{$p.AsOf}}{{end}}{{if $p.Note}} · {{$p.Note}}{{end}}</div>
|
<div class="n" style="font-size:13px;color:var(--dim);margin-top:6px">{{with .FullName}}{{.}} · {{end}}договор {{.Contract}}{{if and (ne .View "archive") $p.AsOf}} · данные на {{$p.AsOf}}{{end}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<span class="stat"><span class="lbl" style="color:var(--dim)">Бумаги</span><span class="v">{{fmtmoney $p.Securities}} ₽</span></span>
|
<span class="stat"><span class="lbl" style="color:var(--dim)">Бумаги</span><span class="v">{{fmtmoney $p.Securities}} ₽</span></span>
|
||||||
@@ -176,10 +180,14 @@ input[type=checkbox]{width:20px;min-height:20px;height:20px}
|
|||||||
{{if .Structure}}<section class="sec" style="margin-bottom:18px">
|
{{if .Structure}}<section class="sec" style="margin-bottom:18px">
|
||||||
<div class="sec-h"><h2 class="lbl" style="font-size:12px;letter-spacing:.14em">Структура портфеля</h2></div>
|
<div class="sec-h"><h2 class="lbl" style="font-size:12px;letter-spacing:.14em">Структура портфеля</h2></div>
|
||||||
<div class="sbar">{{range .Structure}}<span style="width:{{printf "%.2f" .Share}}%;background:{{.Color}}" title="{{.Kind}}: {{fmtmoney .Value}} ₽"></span>{{end}}</div>
|
<div class="sbar">{{range .Structure}}<span style="width:{{printf "%.2f" .Share}}%;background:{{.Color}}" title="{{.Kind}}: {{fmtmoney .Value}} ₽"></span>{{end}}</div>
|
||||||
<div class="slegend">{{range .Structure}}<span><i style="background:{{.Color}}"></i>{{.Kind}} <b class="n">{{fmtpct1 .Share}}</b> <span class="mut n">{{fmtmoney .Value}} ₽</span></span>{{end}}</div>
|
<div class="slegend">{{range .Structure}}<span class="seg" style="border-left-color:{{.Color}}">
|
||||||
|
<span class="t"><i style="background:{{.Color}}"></i>{{.Kind}}</span>
|
||||||
|
<span class="p">{{fmtpct1 .Share}}</span>
|
||||||
|
<span class="v">{{fmtmoney .Value}} ₽</span>
|
||||||
|
</span>{{end}}</div>
|
||||||
</section>{{end}}
|
</section>{{end}}
|
||||||
|
|
||||||
{{if $p.Cash}}<section class="sec" style="margin-bottom:18px">
|
{{if and .ShowCash $p.Cash}}<section class="sec" style="margin-bottom:18px">
|
||||||
<div class="sec-h"><h2 class="lbl" style="font-size:12px;letter-spacing:.14em">Денежные средства</h2></div>
|
<div class="sec-h"><h2 class="lbl" style="font-size:12px;letter-spacing:.14em">Денежные средства</h2></div>
|
||||||
<div style="display:flex;gap:8px;flex-wrap:wrap">{{range $p.Cash}}<span class="chip" style="font-size:13px;text-transform:none;letter-spacing:0"><b class="n">{{fmtmoney .Amount}}</b> {{if eq .Currency "SUR"}}₽{{else}}{{.Currency}}{{end}}</span>{{end}}</div>
|
<div style="display:flex;gap:8px;flex-wrap:wrap">{{range $p.Cash}}<span class="chip" style="font-size:13px;text-transform:none;letter-spacing:0"><b class="n">{{fmtmoney .Amount}}</b> {{if eq .Currency "SUR"}}₽{{else}}{{.Currency}}{{end}}</span>{{end}}</div>
|
||||||
</section>{{end}}
|
</section>{{end}}
|
||||||
@@ -198,6 +206,7 @@ input[type=checkbox]{width:20px;min-height:20px;height:20px}
|
|||||||
<td class="num">{{if gt .Share 0.0}}{{fmtpct1 .Share}}{{else}}—{{end}}</td>
|
<td class="num">{{if gt .Share 0.0}}{{fmtpct1 .Share}}{{else}}—{{end}}</td>
|
||||||
</tr>{{end}}</tbody></table></div>
|
</tr>{{end}}</tbody></table></div>
|
||||||
{{else}}<p class="empty">{{if eq .View "archive"}}На эту дату бумаг на счёте не было.{{else}}Бумаг на счёте нет.{{end}}</p>{{end}}
|
{{else}}<p class="empty">{{if eq .View "archive"}}На эту дату бумаг на счёте не было.{{else}}Бумаг на счёте нет.{{end}}</p>{{end}}
|
||||||
|
{{with $p.Note}}<p class="mut" style="margin-top:10px;font-size:12.5px">{{.}}</p>{{end}}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{{if and (ne .View "archive") .Upcoming}}<section class="sec" style="margin-top:22px">
|
{{if and (ne .View "archive") .Upcoming}}<section class="sec" style="margin-top:22px">
|
||||||
|
|||||||
Reference in New Issue
Block a user